123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185 |
- """
- Manage the logic of downloading the pokedex and source images.
- """
- import re
- import json
- import asyncio
- from dataclasses import dataclass, asdict
- from collections import defaultdict
- from aiohttp import ClientSession
- JS_TO_JSON = re.compile(r"\b([a-zA-Z][a-zA-Z0-9]*?):")
- # the dex from showdown assumes only strawberry alcremie, since
- # that's what's in showdown, but we might as well add the rest
- ALCREMIE_SWEETS = [
- "Strawberry", "Berry", "Love", "Star",
- "Clover", "Flower", "Ribbon",
- ]
- # https://bulbapedia.bulbagarden.net/wiki/List_of_Pok%C3%A9mon_with_gender_differences
- # there are some pokemon with notable gender diffs that the dex doesn't cover
- # judgement calls made arbitrarily
- GENDER_DIFFS = (
- "hippopotas", "hippowdon",
- "unfezant",
- "frillish", "jellicent",
- "pyroar",
- # meowstic, indeedee, basculegion, oinkologne are already handled in the dex
- )
- @dataclass
- class Form:
- name: str
- traits: list[str]
- types: list[str]
- color: str
- @dataclass
- class Pokemon:
- num: int
- species: str
- forms: list[Form]
- async def load_pokedex() -> dict:
- async with ClientSession() as session:
- async with session.get("https://play.pokemonshowdown.com/data/pokedex.js") as res:
- res.raise_for_status()
- text = await res.text("utf-8")
- # this is not json of course, but it's close
- # start by taking out the ; and definition
- cleaned = text.replace("exports.BattlePokedex = ", "").strip(";")
- # then convert the keys to strings
- converted = re.sub(JS_TO_JSON, lambda m: f'"{m.group(1)}":', cleaned)
- # and fix Type: Null
- fixed = converted.replace('""Type": Null"', '"Type: Null"')
- # then, parse it
- return json.loads(fixed)
- def get_traits(species: str, kind: str) -> list[str]:
- traits = []
- if kind in ("mega", "mega-x", "mega-y", "primal"):
- traits.extend(("mega", "nostart"))
- if kind in ("gmax", "eternamax", "rapid-strike-gmax"):
- traits.extend(("gmax", "nostart"))
- if kind in ("alola", "galar", "hisui", "galar", "paldea"):
- traits.extend(("regional", kind))
- # special cases
- if species == "Tauros" and "-paldea" in kind:
- # paldean tauros has dumb names
- traits.extend(("regional", "paldea"))
- if species == "Minior" and kind != "meteor":
- # minior can only start the battle in meteor form
- traits.append("nostart")
- if species == "Darmanitan" and "zen" in kind:
- # darmanitan cannot start in zen form
- traits.append("nostart")
- if "galar" in kind:
- # also there's a galar-zen form to handle
- traits.extend(("regional", "galar"))
- if species == "Palafin" and kind == "hero":
- # palafin can only start in zero form
- traits.append("nostart")
- if species == "Gimmighoul" and kind == "roaming":
- # gimmighoul roaming is only in PGO
- traits.append("nostart")
- return list(set(traits))
- def clean_dex(raw: dict) -> dict[int, Pokemon]:
- regrouped = defaultdict(list)
- for key, entry in raw.items():
- isNonstandard = entry.get("isNonstandard", None)
- baseSpecies = entry.get("baseSpecies", None)
- forme = entry.get("forme", None)
- if isNonstandard not in (None, "Past", "Unobtainable"):
- continue # remove CAP etc.
- if baseSpecies in ("Pikachu", "Pichu") and forme is not None:
- continue # remove pikachu spam + spiky ear pichu
- if forme == "Totem":
- continue # remove totem pokemon
- num = entry["num"]
- # non-cosmetic forms get separate entries automatically
- # but keeping the separate unown forms would be ridiculous
- if key != "unown" and len(cosmetic := entry.get("cosmeticFormes", [])) > 0:
- cosmetic.append(f'{entry["name"]}-{entry["baseForme"]}')
- if key == "alcremie":
- # oh god this thing
- cosmetic = [
- f"{cf}-{sweet}"
- for cf in cosmetic
- for sweet in ALCREMIE_SWEETS
- ]
- regrouped[num].extend({
- **entry,
- "forme": cf.replace(" ", "-"),
- "formeKind": "cosmetic",
- } for cf in cosmetic)
- elif key in GENDER_DIFFS:
- regrouped[num].append({
- **entry,
- "forme": f'{entry["name"]}-M',
- "formeKind": "cosmetic",
- })
- regrouped[num].append({
- **entry,
- "forme": f'{entry["name"]}-F',
- "formeKind": "cosmetic",
- })
- else:
- regrouped[num].append({
- **entry,
- "forme": entry["name"],
- "formeKind": entry.get("forme", "base").lower(),
- })
- cleaned = {}
- for i in range(1, max(regrouped.keys()) + 1):
- forms = regrouped[i]
- # double check there's no skipped or duped entries
- assert len(forms) > 0 and i not in cleaned
- species = forms[0].get("baseSpecies", forms[0]["name"])
- cleaned[i] = Pokemon(
- num=i,
- species=species,
- forms=[
- Form(
- name=f.get("forme", f["name"]),
- traits=get_traits(species, f["formeKind"]),
- types=f["types"],
- color=f["color"],
- ) for f in forms
- ]
- )
- return cleaned
- async def main(dex_file: str):
- # first download the pokedex
- raw_dex = await load_pokedex()
- # clean and reorganize it
- dex = clean_dex(raw_dex)
- # output dex for auditing
- with open(dex_file, "w") as out:
- json.dump({str(i): asdict(pkmn) for i, pkmn in dex.items()}, out, indent=2)
- # TODO actually progress to images
- if __name__ == "__main__":
- from sys import argv
- dex_file = argv[1] if len(argv) > 1 else "data/pokedex.json"
- asyncio.run(main(dex_file))
|