diff --git a/.github/workflows/refresh-data.yml b/.github/workflows/refresh-data.yml new file mode 100644 index 0000000..09e0a98 --- /dev/null +++ b/.github/workflows/refresh-data.yml @@ -0,0 +1,40 @@ +name: Refresh data snapshots + +# Re-fetches Pokémon base stats and the released-shiny list into data/ so the +# site does not depend on PoGoAPI being up at page load. Also reports curated +# CP values that have drifted from current base stats — check the run log. + +on: + schedule: + - cron: '17 6 * * 1' # Mondays, 06:17 UTC + workflow_dispatch: + +permissions: + contents: write + +jobs: + refresh: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Fetch and trim snapshots + run: npm run refresh-data + + - name: Run tests against the refreshed data + run: node --test + + - name: Commit if anything changed + run: | + if [[ -z "$(git status --porcelain data/)" ]]; then + echo "Snapshots already current — nothing to commit." + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add data/ + git commit -m "Refresh Pokémon data snapshots" + git push diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..6f9d7d2 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,18 @@ +name: Tests + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + # No dependencies to install — the suite runs on Node's built-in test runner. + - run: node --test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d7a467a --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +node_modules/ + +# Local editor/tooling config — contains machine-specific absolute paths +.claude/ diff --git a/README.md b/README.md index 6e0db75..dc2bde7 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,140 @@ -# **Project Title: Sundance deGen** +# Sundance deGen -## Project Goal: -To create a web application that simplifies the creation of event descriptions for Pokémon GO meetups hosted on Niantic's Campfire. The tool aims to be adaptable to various event types and bonuses while incorporating relevant meetup data streamlining the event description process. +A browser tool that writes Pokémon GO meetup descriptions for [Niantic's Campfire](https://niantic.helpshift.com/hc/en/19-campfire/), so organisers stop retyping the same post every month. -## Project Background: -The repetitive task of manually creating and updating event descriptions for Pokémon GO meetups, often involving significant copy-pasting month after month, motivated the development of Sundance deGen. This tool was conceived to minimize user input and simplify the generation of consistent event descriptions, freeing up time and reducing the potential for errors. +Paste a month's event schedule as a markdown table and get a finished, copy-ready description for every event — with the Pokémon artwork, 100% IV catch CP, and shiny availability filled in automatically. Or build one event at a time by hand. -## Adaptability: -deGen's adaptable design allows for expansion to support various communities by customizing the description output format. Future updates will also ensure compatibility with new event types and bonuses, maintaining its relevance for event organizers. +Vanilla HTML/CSS/JS. No build step, no dependencies, no framework — just static files. + +--- + +## Two ways to use it + +### Bulk Import (the fast path) + +Paste a markdown table of events and hit **Generate All Descriptions**. Each row becomes a finished description. + +The table needs three columns — event name, details, date — and a header row: + +```markdown +| Event Name | Details | Date | +| --- | --- | --- | +| Ho-Oh Raid Hour | Boosted shiny raids | August 12 | +| Charmander Community Day | 3x Catch Stardust, +1 special trade and half trade cost | August 16 | +``` + +From each row the tool works out: + +| Field | How | +| --- | --- | +| Event type | Matched against the known types in the event name | +| Pokémon | The part of the name that isn't the event type (hyphenated names like `Ho-Oh` and `Porygon-Z` are handled) | +| Date | Month + day; the year rolls forward if the month has already passed | +| Bonuses | Keyword-matched from the details column, capped at the event type's limit | +| Shiny | Looked up against the released-shiny list, falling back to whether "shiny" appears in the details | +| Catch CP | Curated table first, then computed from base stats | +| Featured attack | Pulled from "gets"/"learns" phrasing in the details | + +Rows it can't read are reported inline as a **Skipped** notice and don't stop the rest of the batch. + +### Manual builder + +Click an event type, choose how many descriptions you need (up to 20), and fill in the fields. The Pokémon field autocompletes — arrow keys and Enter work, as does the mouse — and choosing a name fills in CP and shiny status. + +Your pasted event list and location settings survive a refresh. + +--- + +## Event types + +| Event type | Time | Max bonuses | Extra fields | +| --- | --- | --- | --- | +| Spotlight Hour | 6-7PM | 2 | — | +| Raid Hour | 6-7PM | — | 100% CP, weather-boosted CP | +| Community Day | 2-5PM | 4 | Featured attack | +| Community Day Classic | 2-5PM | 4 | Featured attack | +| Raid Day | 2-5PM | 4 | 100% CP, weather-boosted CP, featured attack | +| Hatch Day | 2-5PM | 3 | — | +| Research Day | 2-5PM | 2 | — | +| Max Battles | 2-5PM | 5 | 100% CP | + +Location defaults to **Sundance Park**; switch to **Custom** for anywhere else. + +## Output + +``` +Ho-Oh Raid Hour +🎈 Join us at Sundance Park on August 12th for the Ho-Oh Raid Hour from 6-7PM 💃☀️🕺 + +💯 - 2207 / WB - 2759 + +If you're lucky, you might encounter a shiny one ✨ +✅ "Check in" on Campfire when you arrive +``` + +Each result comes with the Pokémon's artwork and a **Copy** button. + +--- + +## Running locally + +The scripts are ES modules, so opening `index.html` off disk won't work — it needs to be served over HTTP: + +```bash +python3 -m http.server 8080 +``` + +Then open . Any static server works. + +## Tests + +Pure logic — table parsing, name extraction, bonus matching, date handling, description rendering — is separated from the DOM in `parse.js` and covered by Node's built-in test runner. No dependencies to install: + +```bash +npm test +``` + +## Project layout + +| File | What's in it | +| --- | --- | +| `index.html` | Markup and the full stylesheet (Observation Deck design tokens, dark-only) | +| `config.js` | Event definitions, bonus keyword patterns, and every fixed phrase in the output | +| `parse.js` | Pure logic — parsing, formatting, and the single description renderer | +| `data.js` | Everything that touches the network, plus the data-source status chip | +| `main.js` | DOM wiring only | +| `cpOverrides.js` | Manual CP pins for cases base stats can't express — normally empty | +| `data/` | Vendored API snapshots, refreshed weekly by CI | +| `scripts/refresh-data.js` | Fetches and trims those snapshots | +| `test/` | Test suite | + +## Adapting it for another community + +`config.js` is the only file you need. `constants` holds every fixed phrase in the generated text; `eventConfig` defines which event types exist, when they run, which bonuses each may carry, and which extra fields it collects. A type's `specialFields` drives both the form and the description — adding `"hundo"` makes the CP line appear, `"attack"` adds the featured-attack line — so no other file hardcodes wording or event names. + +## Data sources + +| Source | Used for | +| --- | --- | +| [PokéAPI](https://pokeapi.co) | Autocomplete name list; Pokédex number for artwork | +| [pokemon.com](https://www.pokemon.com) | Official artwork | +| [PoGoAPI](https://pogoapi.net) | Base stats for CP calculation; released-shiny list | + +Catch CP is `(attack + 15) × √(defense + 15) × √(stamina + 15) × CPM² ÷ 10`, using CPM 0.5974 (level 20, standard raid catch) and 0.667934 (level 25, weather boosted). + +PoGoAPI data is **vendored into `data/`** rather than fetched at page load, so an outage there can't quietly strip the CP and shiny lines out of your descriptions. A weekly GitHub Action re-fetches it; the live API is consulted only for Pokémon released since the last snapshot. The status chip under the title shows which source is answering. + +**CP is derived, never stored.** There is no table of CP values to maintain, so none can go stale. Lookups resolve cheapest-first: `cpOverrides.js` → snapshot → live API. + +Two details make deriving reliable: + +- **The catchable form is chosen explicitly.** PoGoAPI doesn't list base forms first — Galarian Articuno precedes Normal, Crowned Sword Zacian precedes Hero — so `CATCHABLE_FORMS` in `parse.js` picks the form you actually catch rather than whichever came first. +- **Form-qualified names are indexed under their own key.** "Deoxys Attack" and "Giratina (Origin)" have no entry of their own in the API, which models them as name + form. Both spellings of a regional form work: `Exeggutor (Alolan)` and `Exeggutor Alola` resolve to the same stats. + +`cpOverrides.js` is the escape hatch for a value a human has to judge. It is currently empty — every name the tool has ever needed derives correctly. Prefer fixing the derivation over adding an override, since an override is a value nothing will re-check. `npm run refresh-data` reports overrides that have become redundant or unresolvable. + +## Known limitations + +- Bonus detection is keyword-based, so unusual phrasing in the details column may be missed. Check the generated bonus list before posting. +- Bulk results can't be edited in place — to change a parsed value, rebuild that event in the manual builder. +- Changing the "how many descriptions" count rebuilds the form and clears anything already typed in. diff --git a/config.js b/config.js new file mode 100644 index 0000000..2dc8a01 --- /dev/null +++ b/config.js @@ -0,0 +1,125 @@ +/** + * Event definitions and the wording of the generated description. + * + * This is the file to edit to adapt deGen to another community: `constants` + * controls every fixed phrase in the output, and `eventConfig` controls which + * event types exist, when they run, which bonuses they may carry, and which + * extra fields they collect. No other file hardcodes description wording. + */ + +export const constants = { + checkInText: '✅ "Check in" on Campfire when you arrive', + eventEmojis: "💃☀️🕺", + shinyText: "If you're lucky, you might encounter a shiny one ✨\n", + bonusHeaderSingle: "————Event Bonus————", + bonusHeaderMultiple: "————Event Bonuses————", + defaultLocation: "Sundance Park" +}; + +/** + * specialFields drives both the form fields rendered and the lines that appear + * in the description — "hundo"/"whundo" produce the CP line, "attack" produces + * the featured-attack line. Nothing keys off the event *name*. + */ +export const eventConfig = { + "Spotlight Hour": { + time: "6-7PM", + bonuses: ["2x Catch Stardust", "2x Catch XP", "2x Catch Candy", "2x Transfer Candy", "2x Evolution XP"], + specialFields: [], + maxBonuses: 2 + }, + "Raid Hour": { + time: "6-7PM", + bonuses: [], + specialFields: ["hundo", "whundo"], + maxBonuses: 0 + }, + "Community Day": { + time: "2-5PM", + bonuses: ["1/2 Hatch Distance", "1/4 Hatch Distance", "2x Catch XP & Stardust", "2x Catch Candy & XL Chance", "3x Catch Stardust", "3 HR Lures / Incense", "+1 Special Trade & Half Trade Cost"], + specialFields: ["attack"], + maxBonuses: 4 + }, + "Community Day Classic": { + time: "2-5PM", + bonuses: ["1/4 Hatch Distance", "3 HR Lures / Incense", "5 Photobomb Encounters", "Extra Special Trade"], + specialFields: ["attack"], + maxBonuses: 4 + }, + "Raid Day": { + time: "2-5PM", + bonuses: ["Increased shiny chance", "5 free raid passes by spinning gyms, 6 total", "Remote raids increased to 20", "Extra Raid Bonus", "1.5x XP from Raids"], + specialFields: ["hundo", "whundo", "attack"], + maxBonuses: 4 + }, + "Hatch Day": { + time: "2-5PM", + bonuses: ["Increased shiny chance", "1/4 Hatch Distance", "1/2 Hatch Distance"], + specialFields: [], + maxBonuses: 3 + }, + "Research Day": { + time: "2-5PM", + bonuses: ["Increased shiny chance"], + specialFields: [], + maxBonuses: 2 + }, + "Max Battles": { + time: "2-5PM", + bonuses: ["1/4 Adventure Distance for MP", "MP Collection raised to 1600", "8x MP from Power Spots", "2x MP for Exploring", "+2 Special Trades"], + specialFields: ["hundo"], + maxBonuses: 5 + } +}; + +/** Event-name fragments the bulk parser maps to a type. Longest match wins, so order matters. */ +export const eventTypeAliases = [ + ["community day classic", "Community Day Classic"], + ["community day", "Community Day"], + ["hatch day", "Hatch Day"], + ["raid day", "Raid Day"], + ["raid hour", "Raid Hour"], + ["spotlight hour", "Spotlight Hour"], + ["research day", "Research Day"], + ["max battle", "Max Battles"] +]; + +/** + * Bonus detection for the bulk importer. A pattern may offer several spellings; + * the first one the matched event type actually declares is the one used. + */ +export const bonusPatterns = [ + { re: /3[x×]\s*catch stardust|3[x×]\s*stardust/i, bonus: "3x Catch Stardust" }, + { re: /2[x×]\s*catch xp\s*[&]\s*stardust|2[x×]\s*xp\s*[&]\s*stardust/i, bonus: "2x Catch XP & Stardust" }, + { re: /2[x×]\s*catch xp|2[x×]\s*xp(?!\s*[&])/i, bonus: "2x Catch XP" }, + { re: /2[x×]\s*catch stardust|2[x×]\s*stardust/i, bonus: "2x Catch Stardust" }, + { re: /2[x×]\s*candy\s*[&]\s*xl|2[x×]\s*candy.*xl/i, bonus: "2x Catch Candy & XL Chance" }, + { re: /2[x×]\s*catch candy|2[x×]\s*candy(?!\s*[&xl])/i, bonus: "2x Catch Candy" }, + { re: /2[x×]\s*transfer candy/i, bonus: "2x Transfer Candy" }, + { re: /2[x×]\s*evolution xp|2[x×]\s*evolv/i, bonus: "2x Evolution XP" }, + { re: /[¼]|1\/4\s*hatch/i, bonus: "1/4 Hatch Distance" }, + { re: /[½]|1\/2\s*hatch/i, bonus: "1/2 Hatch Distance" }, + { re: /3\s*hr\s*lures?/i, bonus: "3 HR Lures / Incense" }, + { re: /boosted shiny|increased shiny/i, bonus: "Increased shiny chance" }, + { re: /5\s*photobomb/i, bonus: "5 Photobomb Encounters" }, + // Community Day calls this "+1 Special Trade & Half Trade Cost"; CD Classic + // calls it "Extra Special Trade". Offer both spellings. + { re: /extra special trade|special trade|half trade cost/i, bonus: ["Extra Special Trade", "+1 Special Trade & Half Trade Cost"] }, + { re: /free raid pass/i, bonus: "5 free raid passes by spinning gyms, 6 total" }, + { re: /remote raid/i, bonus: "Remote raids increased to 20" }, + { re: /1\.5[x×]\s*xp from raids/i, bonus: "1.5x XP from Raids" }, + { re: /extra raid bonus/i, bonus: "Extra Raid Bonus" }, + { re: /1\/4.*adventure|adventure.*1\/4/i, bonus: "1/4 Adventure Distance for MP" }, + { re: /mp.*1600|1600.*mp/i, bonus: "MP Collection raised to 1600" }, + { re: /8[x×].*mp|8[x×].*power spot/i, bonus: "8x MP from Power Spots" }, + { re: /2[x×].*mp.*explor/i, bonus: "2x MP for Exploring" } +]; + +/** + * Combat Power multipliers for the two catch levels shown in raid descriptions. + * L20 is a standard raid catch; L25 is the weather-boosted catch. + */ +export const CPM = { + level20: 0.5974, + level25: 0.667934 +}; diff --git a/cpOverrides.js b/cpOverrides.js new file mode 100644 index 0000000..826404a --- /dev/null +++ b/cpOverrides.js @@ -0,0 +1,28 @@ +/** + * Manually pinned catch CP. + * + * Catch CP is normally *derived* from current Pokémon GO base stats, so it can + * never go stale — see data.js. This map exists only for cases the stat data + * cannot express on its own, and it is checked before anything else. + * + * Add an entry when, and only when, the derived value is wrong for a reason a + * human has to judge. The known category is **which form you actually catch**: + * Zacian's base stats resolve to Crowned Sword, but raids give Hero of Many + * Battles. That particular case is now handled generically by CATCHABLE_FORMS + * in parse.js, which is the better fix whenever the rule generalises. + * + * Prefer fixing the derivation over adding an override. An override is a value + * nothing will ever re-check — exactly the staleness this file replaced. + * + * Format matches the derived shape: + * + * "Some Pokémon": { hundo: 1234, whundo: 1543 }, // why, and when checked + * + * hundo = level 20 catch (standard raid), 15/15/15 + * whundo = level 25 catch (weather boosted), 15/15/15 + */ +export const cpOverrides = { + // Empty by design. Every name the tool has ever needed currently resolves + // from base stats, including form-qualified ones like "Deoxys Attack" and + // "Giratina (Origin)". +}; diff --git a/data.js b/data.js new file mode 100644 index 0000000..f6aac9d --- /dev/null +++ b/data.js @@ -0,0 +1,215 @@ +/** + * Everything that reaches outside the page. + * + * Catch CP is derived from Pokémon GO base stats rather than stored, so it + * cannot drift out of date. Lookups resolve in three tiers, cheapest first: + * 1. cpOverrides.js — manual pins; normally empty + * 2. the vendored snapshot in data/ — same origin, refreshed weekly by CI + * 3. the live APIs — only when the snapshot misses + * + * Tier 3 exists so a Pokémon released after the last snapshot still resolves. + * Every call is best-effort: callers get null rather than an exception, and the + * status chip tells the user which tier is actually answering. + */ + +import { calcCatchCP, normalizeName, lookupCPOverride, indexBaseStats, pokeApiSlug } from './parse.js'; + +const TIMEOUT_MS = 8000; + +const SNAPSHOT = { + stats: './data/pokemon-stats.json', + shiny: './data/shiny-pokemon.json' +}; + +const LIVE = { + names: 'https://pokeapi.co/api/v2/pokemon?limit=2000', + stats: 'https://pogoapi.net/api/v1/pokemon_stats.json', + shiny: 'https://pogoapi.net/api/v1/shiny_pokemon.json', + pokemon: name => `https://pokeapi.co/api/v2/pokemon/${pokeApiSlug(name)}`, + artwork: id => `https://www.pokemon.com/static-assets/content-assets/cms2/img/pokedex/full/${id}.png` +}; + +// ---------------------------------------------------------------- status ---- + +const status = { source: 'loading', generated: null }; +const listeners = new Set(); + +export function getStatus() { + return { ...status }; +} + +/** Subscribe to data-source changes; fires immediately with the current state. */ +export function onStatusChange(fn) { + listeners.add(fn); + fn(getStatus()); + return () => listeners.delete(fn); +} + +function setStatus(patch) { + Object.assign(status, patch); + for (const fn of listeners) fn(getStatus()); +} + +// ----------------------------------------------------------------- fetch ---- + +async function getJSON(url) { + const response = await fetch(url, { signal: AbortSignal.timeout(TIMEOUT_MS) }); + if (!response.ok) throw new Error(`${response.status} for ${url}`); + return response.json(); +} + +/** Run a loader once and cache the promise, so concurrent callers share one request. */ +function once(fn) { + let promise = null; + return () => (promise ??= fn().catch(error => { + promise = null; // let a later call retry + throw error; + })); +} + +// ------------------------------------------------------------ base stats ---- + +/** Display names from whichever stats source loaded — the autocomplete fallback. */ +let statsNames = []; + +const loadSnapshotStats = once(async () => { + const snapshot = await getJSON(SNAPSHOT.stats); + setStatus({ source: 'snapshot', generated: snapshot.generated ?? null }); + const pokemon = snapshot.pokemon ?? []; + statsNames = pokemon.map(p => p.name); + return indexBaseStats(pokemon); +}); + +const loadLiveStats = once(async () => { + const live = await getJSON(LIVE.stats); + const pokemon = live.map(p => ({ + name: p.pokemon_name, atk: p.base_attack, def: p.base_defense, sta: p.base_stamina, form: p.form + })); + if (!statsNames.length) statsNames = [...new Set(pokemon.map(p => p.name))]; + return indexBaseStats(pokemon); +}); + +const ensureStats = once(async () => { + try { + return await loadSnapshotStats(); + } catch { + // No snapshot deployed (or unreadable) — fall back to the live API. + try { + const index = await loadLiveStats(); + setStatus({ source: 'live', generated: null }); + return index; + } catch { + setStatus({ source: 'offline', generated: null }); + return null; + } + } +}); + +/** + * Start loading the data snapshot without needing a lookup to trigger it, so + * the status chip settles on page load rather than on first use. + */ +export function primeData() { + return ensureStats(); +} + +/** + * Catch CP for a Pokémon, or null if it cannot be resolved. + * + * Derived from current base stats, so it cannot go stale. A manual override + * wins if one exists, but that map is normally empty — see cpOverrides.js. + */ +export async function getCatchCP(name) { + if (!name) return null; + + const pinned = lookupCPOverride(name); + if (pinned) return pinned; + + const target = normalizeName(name); + const stats = await ensureStats(); + const hit = stats?.get(target); + if (hit) return calcCatchCP(hit); + + // Absent from the snapshot may just mean "released since it was taken". + if (status.source === 'snapshot') { + try { + const live = await loadLiveStats(); + const liveHit = live.get(target); + if (liveHit) return calcCatchCP(liveHit); + } catch { /* live unavailable — nothing more to try */ } + } + return null; +} + +// ------------------------------------------------------------ shiny list ---- + +const loadSnapshotShiny = once(async () => { + const snapshot = await getJSON(SNAPSHOT.shiny); + return new Set((snapshot.names ?? []).map(normalizeName)); +}); + +const loadLiveShiny = once(async () => { + const live = await getJSON(LIVE.shiny); + return new Set(Object.values(live).map(entry => normalizeName(entry.name))); +}); + +const ensureShiny = once(async () => { + try { + return await loadSnapshotShiny(); + } catch { + try { + return await loadLiveShiny(); + } catch { + return null; + } + } +}); + +/** true / false if known, null if no shiny data could be loaded at all. */ +export async function isShinyReleased(name) { + if (!name) return null; + const set = await ensureShiny(); + if (!set) return null; + + const target = normalizeName(name); + if (set.has(target)) return true; + + // A miss against the snapshot might be a recent release; confirm against live. + if (status.source === 'snapshot') { + try { + const live = await loadLiveShiny(); + return live.has(target); + } catch { /* fall through to the snapshot's answer */ } + } + return false; +} + +// ------------------------------------------------------------ name list ---- + +/** + * Full Pokémon name list for autocomplete. Falls back to the names in the stats + * snapshot, which is same-origin and therefore available whenever the page is. + */ +export async function loadPokemonNames() { + try { + const data = await getJSON(LIVE.names); + return data.results.map(p => p.name.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase())); + } catch (error) { + console.error('Pokémon name list unavailable, falling back to the stats snapshot:', error); + await ensureStats(); + return [...statsNames].sort(); + } +} + +// -------------------------------------------------------------- artwork ---- + +/** Official artwork URL for a Pokémon, or null if it can't be resolved. */ +export async function fetchPokemonImage(name) { + try { + const data = await getJSON(LIVE.pokemon(name)); + return LIVE.artwork(String(data.id).padStart(3, '0')); + } catch (error) { + console.error(`No artwork for ${name}:`, error); + return null; + } +} diff --git a/data/pokemon-stats.json b/data/pokemon-stats.json new file mode 100644 index 0000000..dce35d2 --- /dev/null +++ b/data/pokemon-stats.json @@ -0,0 +1 @@ +{"generated":"2026-08-02T03:09:47.850Z","source":"https://pogoapi.net/api/v1/pokemon_stats.json","pokemon":[{"name":"Bulbasaur","form":"Fall_2019","atk":118,"def":111,"sta":128},{"name":"Bulbasaur","form":"Normal","atk":118,"def":111,"sta":128},{"name":"Ivysaur","form":"Normal","atk":151,"def":143,"sta":155},{"name":"Venusaur","form":"Copy_2019","atk":198,"def":189,"sta":190},{"name":"Venusaur","form":"Normal","atk":198,"def":189,"sta":190},{"name":"Charmander","form":"Fall_2019","atk":116,"def":93,"sta":118},{"name":"Charmander","form":"Normal","atk":116,"def":93,"sta":118},{"name":"Charmeleon","form":"Normal","atk":158,"def":126,"sta":151},{"name":"Charizard","form":"Copy_2019","atk":223,"def":173,"sta":186},{"name":"Charizard","form":"Normal","atk":223,"def":173,"sta":186},{"name":"Squirtle","form":"Fall_2019","atk":94,"def":121,"sta":127},{"name":"Squirtle","form":"Normal","atk":94,"def":121,"sta":127},{"name":"Wartortle","form":"Normal","atk":126,"def":155,"sta":153},{"name":"Blastoise","form":"Copy_2019","atk":171,"def":207,"sta":188},{"name":"Blastoise","form":"Normal","atk":171,"def":207,"sta":188},{"name":"Caterpie","form":"Normal","atk":55,"def":55,"sta":128},{"name":"Metapod","form":"Normal","atk":45,"def":80,"sta":137},{"name":"Butterfree","form":"Normal","atk":167,"def":137,"sta":155},{"name":"Weedle","form":"Normal","atk":63,"def":50,"sta":120},{"name":"Kakuna","form":"Normal","atk":46,"def":75,"sta":128},{"name":"Beedrill","form":"Normal","atk":169,"def":130,"sta":163},{"name":"Pidgey","form":"Normal","atk":85,"def":73,"sta":120},{"name":"Pidgeotto","form":"Normal","atk":117,"def":105,"sta":160},{"name":"Pidgeot","form":"Normal","atk":166,"def":154,"sta":195},{"name":"Rattata","form":"Alola","atk":103,"def":70,"sta":102},{"name":"Rattata","form":"Normal","atk":103,"def":70,"sta":102},{"name":"Raticate","form":"Alola","atk":135,"def":154,"sta":181},{"name":"Raticate","form":"Normal","atk":161,"def":139,"sta":146},{"name":"Spearow","form":"Normal","atk":112,"def":60,"sta":120},{"name":"Fearow","form":"Normal","atk":182,"def":133,"sta":163},{"name":"Ekans","form":"Normal","atk":110,"def":97,"sta":111},{"name":"Arbok","form":"Normal","atk":167,"def":153,"sta":155},{"name":"Pikachu","form":"Adventure_hat_2020","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Copy_2019","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Costume_2020","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Diwali_2024","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Doctor","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Fall_2019","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Flying_01","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Flying_02","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Flying_03","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Flying_04","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Flying_5th_anniv","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Flying_okinawa","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Gofest_2022","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Gofest_2024_mtiara","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Gofest_2024_stiara","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Gofest_2025_goggles_blue","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Gofest_2025_goggles_red","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Gofest_2025_goggles_yellow","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Gofest_2025_monocle_blue","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Gofest_2025_monocle_red","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Gofest_2025_monocle_yellow","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Gotour_2024_a","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Gotour_2024_a_02","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Gotour_2024_b","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Gotour_2024_b_02","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Gotour_2025_a","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Gotour_2025_a_02","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Gotour_2025_b","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Gotour_2025_b_02","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Horizons","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Jeju","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Kariyushi","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Kurta","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Normal","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Pop_star","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Rock_star","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Summer_2023_a","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Summer_2023_b","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Summer_2023_c","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Summer_2023_d","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Summer_2023_e","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Tshirt_01","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Tshirt_02","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Tshirt_03","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Vs_2019","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Wcs_2022","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Wcs_2023","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Wcs_2024","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Wcs_2025","atk":112,"def":96,"sta":111},{"name":"Pikachu","form":"Winter_2020","atk":112,"def":96,"sta":111},{"name":"Raichu","form":"Alola","atk":201,"def":154,"sta":155},{"name":"Raichu","form":"Normal","atk":193,"def":151,"sta":155},{"name":"Sandshrew","form":"Alola","atk":125,"def":129,"sta":137},{"name":"Sandshrew","form":"Normal","atk":126,"def":120,"sta":137},{"name":"Sandslash","form":"Alola","atk":177,"def":195,"sta":181},{"name":"Sandslash","form":"Normal","atk":182,"def":175,"sta":181},{"name":"Nidoran♀","form":"Normal","atk":86,"def":89,"sta":146},{"name":"Nidorina","form":"Normal","atk":117,"def":120,"sta":172},{"name":"Nidoqueen","form":"Normal","atk":180,"def":173,"sta":207},{"name":"Nidoran♂","form":"Normal","atk":105,"def":76,"sta":130},{"name":"Nidorino","form":"Normal","atk":137,"def":111,"sta":156},{"name":"Nidoking","form":"Normal","atk":204,"def":156,"sta":191},{"name":"Clefairy","form":"Normal","atk":107,"def":108,"sta":172},{"name":"Clefable","form":"Normal","atk":178,"def":162,"sta":216},{"name":"Vulpix","form":"Alola","atk":96,"def":109,"sta":116},{"name":"Vulpix","form":"Normal","atk":96,"def":109,"sta":116},{"name":"Ninetales","form":"Alola","atk":170,"def":193,"sta":177},{"name":"Ninetales","form":"Normal","atk":169,"def":190,"sta":177},{"name":"Jigglypuff","form":"Normal","atk":80,"def":41,"sta":251},{"name":"Wigglytuff","form":"Normal","atk":156,"def":90,"sta":295},{"name":"Zubat","form":"Normal","atk":83,"def":73,"sta":120},{"name":"Golbat","form":"Normal","atk":161,"def":150,"sta":181},{"name":"Oddish","form":"Normal","atk":131,"def":112,"sta":128},{"name":"Gloom","form":"Normal","atk":153,"def":136,"sta":155},{"name":"Vileplume","form":"Normal","atk":202,"def":167,"sta":181},{"name":"Paras","form":"Normal","atk":121,"def":99,"sta":111},{"name":"Parasect","form":"Normal","atk":165,"def":146,"sta":155},{"name":"Venonat","form":"Normal","atk":100,"def":100,"sta":155},{"name":"Venomoth","form":"Normal","atk":179,"def":143,"sta":172},{"name":"Diglett","form":"Alola","atk":108,"def":81,"sta":67},{"name":"Diglett","form":"Normal","atk":109,"def":78,"sta":67},{"name":"Dugtrio","form":"Alola","atk":201,"def":142,"sta":111},{"name":"Dugtrio","form":"Normal","atk":167,"def":136,"sta":111},{"name":"Meowth","form":"Alola","atk":99,"def":78,"sta":120},{"name":"Meowth","form":"Galarian","atk":115,"def":92,"sta":137},{"name":"Meowth","form":"Normal","atk":92,"def":78,"sta":120},{"name":"Persian","form":"Alola","atk":158,"def":136,"sta":163},{"name":"Persian","form":"Normal","atk":150,"def":136,"sta":163},{"name":"Psyduck","form":"Normal","atk":122,"def":95,"sta":137},{"name":"Psyduck","form":"Swim_2025","atk":122,"def":95,"sta":137},{"name":"Golduck","form":"Normal","atk":191,"def":162,"sta":190},{"name":"Mankey","form":"Normal","atk":148,"def":82,"sta":120},{"name":"Primeape","form":"Normal","atk":207,"def":138,"sta":163},{"name":"Growlithe","form":"Hisuian","atk":142,"def":92,"sta":155},{"name":"Growlithe","form":"Normal","atk":136,"def":93,"sta":146},{"name":"Arcanine","form":"Hisuian","atk":232,"def":165,"sta":216},{"name":"Arcanine","form":"Normal","atk":227,"def":166,"sta":207},{"name":"Poliwag","form":"Normal","atk":101,"def":82,"sta":120},{"name":"Poliwhirl","form":"Normal","atk":130,"def":123,"sta":163},{"name":"Poliwrath","form":"Normal","atk":182,"def":184,"sta":207},{"name":"Abra","form":"Normal","atk":195,"def":82,"sta":93},{"name":"Kadabra","form":"Normal","atk":232,"def":117,"sta":120},{"name":"Alakazam","form":"Normal","atk":271,"def":167,"sta":146},{"name":"Machop","form":"Normal","atk":137,"def":82,"sta":172},{"name":"Machoke","form":"Normal","atk":177,"def":125,"sta":190},{"name":"Machamp","form":"Normal","atk":234,"def":159,"sta":207},{"name":"Bellsprout","form":"Normal","atk":139,"def":61,"sta":137},{"name":"Weepinbell","form":"Normal","atk":172,"def":92,"sta":163},{"name":"Victreebel","form":"Normal","atk":207,"def":135,"sta":190},{"name":"Tentacool","form":"Normal","atk":97,"def":149,"sta":120},{"name":"Tentacruel","form":"Normal","atk":166,"def":209,"sta":190},{"name":"Geodude","form":"Alola","atk":132,"def":132,"sta":120},{"name":"Geodude","form":"Normal","atk":132,"def":132,"sta":120},{"name":"Graveler","form":"Alola","atk":164,"def":164,"sta":146},{"name":"Graveler","form":"Normal","atk":164,"def":164,"sta":146},{"name":"Golem","form":"Alola","atk":211,"def":198,"sta":190},{"name":"Golem","form":"Normal","atk":211,"def":198,"sta":190},{"name":"Ponyta","form":"Galarian","atk":170,"def":127,"sta":137},{"name":"Ponyta","form":"Normal","atk":170,"def":127,"sta":137},{"name":"Rapidash","form":"Galarian","atk":207,"def":162,"sta":163},{"name":"Rapidash","form":"Normal","atk":207,"def":162,"sta":163},{"name":"Slowpoke","form":"2020","atk":109,"def":98,"sta":207},{"name":"Slowpoke","form":"Galarian","atk":109,"def":98,"sta":207},{"name":"Slowpoke","form":"Normal","atk":109,"def":98,"sta":207},{"name":"Slowbro","form":"2021","atk":177,"def":180,"sta":216},{"name":"Slowbro","form":"Galarian","atk":182,"def":156,"sta":216},{"name":"Slowbro","form":"Normal","atk":177,"def":180,"sta":216},{"name":"Magnemite","form":"Normal","atk":165,"def":121,"sta":93},{"name":"Magneton","form":"Normal","atk":223,"def":169,"sta":137},{"name":"Farfetch’d","form":"Galarian","atk":174,"def":114,"sta":141},{"name":"Farfetch’d","form":"Normal","atk":124,"def":115,"sta":141},{"name":"Doduo","form":"Normal","atk":158,"def":83,"sta":111},{"name":"Dodrio","form":"Normal","atk":218,"def":140,"sta":155},{"name":"Seel","form":"Normal","atk":85,"def":121,"sta":163},{"name":"Dewgong","form":"Normal","atk":139,"def":177,"sta":207},{"name":"Grimer","form":"Alola","atk":135,"def":90,"sta":190},{"name":"Grimer","form":"Normal","atk":135,"def":90,"sta":190},{"name":"Muk","form":"Alola","atk":190,"def":172,"sta":233},{"name":"Muk","form":"Normal","atk":190,"def":172,"sta":233},{"name":"Shellder","form":"Normal","atk":116,"def":134,"sta":102},{"name":"Cloyster","form":"Normal","atk":186,"def":256,"sta":137},{"name":"Gastly","form":"Normal","atk":186,"def":67,"sta":102},{"name":"Haunter","form":"Normal","atk":223,"def":107,"sta":128},{"name":"Gengar","form":"Costume_2020","atk":261,"def":149,"sta":155},{"name":"Gengar","form":"Normal","atk":261,"def":149,"sta":155},{"name":"Onix","form":"Normal","atk":85,"def":232,"sta":111},{"name":"Drowzee","form":"Normal","atk":89,"def":136,"sta":155},{"name":"Hypno","form":"Normal","atk":144,"def":193,"sta":198},{"name":"Krabby","form":"Normal","atk":181,"def":124,"sta":102},{"name":"Kingler","form":"Normal","atk":240,"def":181,"sta":146},{"name":"Voltorb","form":"Hisuian","atk":109,"def":111,"sta":120},{"name":"Voltorb","form":"Normal","atk":109,"def":111,"sta":120},{"name":"Electrode","form":"Hisuian","atk":176,"def":176,"sta":155},{"name":"Electrode","form":"Normal","atk":173,"def":173,"sta":155},{"name":"Exeggcute","form":"Normal","atk":107,"def":125,"sta":155},{"name":"Exeggutor","form":"Alola","atk":230,"def":153,"sta":216},{"name":"Exeggutor","form":"Normal","atk":233,"def":149,"sta":216},{"name":"Cubone","form":"Normal","atk":90,"def":144,"sta":137},{"name":"Marowak","form":"Alola","atk":144,"def":186,"sta":155},{"name":"Marowak","form":"Normal","atk":144,"def":186,"sta":155},{"name":"Hitmonlee","form":"Normal","atk":224,"def":181,"sta":137},{"name":"Hitmonchan","form":"Normal","atk":193,"def":197,"sta":137},{"name":"Lickitung","form":"Normal","atk":108,"def":137,"sta":207},{"name":"Koffing","form":"Normal","atk":119,"def":141,"sta":120},{"name":"Weezing","form":"Galarian","atk":174,"def":197,"sta":163},{"name":"Weezing","form":"Normal","atk":174,"def":197,"sta":163},{"name":"Rhyhorn","form":"Normal","atk":140,"def":127,"sta":190},{"name":"Rhydon","form":"Normal","atk":222,"def":171,"sta":233},{"name":"Chansey","form":"Normal","atk":60,"def":128,"sta":487},{"name":"Tangela","form":"Normal","atk":183,"def":169,"sta":163},{"name":"Kangaskhan","form":"Normal","atk":181,"def":165,"sta":233},{"name":"Horsea","form":"Normal","atk":129,"def":103,"sta":102},{"name":"Seadra","form":"Normal","atk":187,"def":156,"sta":146},{"name":"Goldeen","form":"Normal","atk":123,"def":110,"sta":128},{"name":"Seaking","form":"Normal","atk":175,"def":147,"sta":190},{"name":"Staryu","form":"Normal","atk":137,"def":112,"sta":102},{"name":"Starmie","form":"Normal","atk":210,"def":184,"sta":155},{"name":"Mr. Mime","form":"Galarian","atk":183,"def":169,"sta":137},{"name":"Mr. Mime","form":"Normal","atk":192,"def":205,"sta":120},{"name":"Scyther","form":"Normal","atk":218,"def":170,"sta":172},{"name":"Jynx","form":"Normal","atk":223,"def":151,"sta":163},{"name":"Electabuzz","form":"Normal","atk":198,"def":158,"sta":163},{"name":"Magmar","form":"Normal","atk":206,"def":154,"sta":163},{"name":"Pinsir","form":"Normal","atk":238,"def":182,"sta":163},{"name":"Tauros","form":"Normal","atk":198,"def":183,"sta":181},{"name":"Tauros","form":"Paldea_aqua","atk":210,"def":193,"sta":181},{"name":"Tauros","form":"Paldea_blaze","atk":210,"def":193,"sta":181},{"name":"Tauros","form":"Paldea_combat","atk":210,"def":193,"sta":181},{"name":"Magikarp","form":"Normal","atk":29,"def":85,"sta":85},{"name":"Gyarados","form":"Normal","atk":237,"def":186,"sta":216},{"name":"Lapras","form":"Costume_2020","atk":165,"def":174,"sta":277},{"name":"Lapras","form":"Normal","atk":165,"def":174,"sta":277},{"name":"Ditto","form":"Normal","atk":91,"def":91,"sta":134},{"name":"Eevee","form":"Gofest_2024_mtiara","atk":104,"def":114,"sta":146},{"name":"Eevee","form":"Gofest_2024_stiara","atk":104,"def":114,"sta":146},{"name":"Eevee","form":"Normal","atk":104,"def":114,"sta":146},{"name":"Vaporeon","form":"Normal","atk":205,"def":161,"sta":277},{"name":"Jolteon","form":"Normal","atk":232,"def":182,"sta":163},{"name":"Flareon","form":"Normal","atk":246,"def":179,"sta":163},{"name":"Porygon","form":"Normal","atk":153,"def":136,"sta":163},{"name":"Omanyte","form":"Normal","atk":155,"def":153,"sta":111},{"name":"Omastar","form":"Normal","atk":207,"def":201,"sta":172},{"name":"Kabuto","form":"Normal","atk":148,"def":140,"sta":102},{"name":"Kabutops","form":"Normal","atk":220,"def":186,"sta":155},{"name":"Aerodactyl","form":"Normal","atk":221,"def":159,"sta":190},{"name":"Aerodactyl","form":"Summer_2023","atk":221,"def":159,"sta":190},{"name":"Snorlax","form":"Normal","atk":190,"def":169,"sta":330},{"name":"Snorlax","form":"Wildarea_2024","atk":190,"def":169,"sta":330},{"name":"Articuno","form":"Galarian","atk":250,"def":197,"sta":207},{"name":"Articuno","form":"Normal","atk":192,"def":236,"sta":207},{"name":"Zapdos","form":"Galarian","atk":252,"def":189,"sta":207},{"name":"Zapdos","form":"Normal","atk":253,"def":185,"sta":207},{"name":"Moltres","form":"Galarian","atk":202,"def":231,"sta":207},{"name":"Moltres","form":"Normal","atk":251,"def":181,"sta":207},{"name":"Dratini","form":"Normal","atk":119,"def":91,"sta":121},{"name":"Dragonair","form":"Normal","atk":163,"def":135,"sta":156},{"name":"Dragonite","form":"Normal","atk":263,"def":198,"sta":209},{"name":"Mewtwo","form":"A","atk":182,"def":278,"sta":214},{"name":"Mewtwo","form":"Normal","atk":300,"def":182,"sta":214},{"name":"Mew","form":"Normal","atk":210,"def":210,"sta":225},{"name":"Chikorita","form":"Normal","atk":92,"def":122,"sta":128},{"name":"Bayleef","form":"Normal","atk":122,"def":155,"sta":155},{"name":"Meganium","form":"Normal","atk":168,"def":202,"sta":190},{"name":"Cyndaquil","form":"Normal","atk":116,"def":93,"sta":118},{"name":"Quilava","form":"Normal","atk":158,"def":126,"sta":151},{"name":"Typhlosion","form":"Hisuian","atk":238,"def":172,"sta":177},{"name":"Typhlosion","form":"Normal","atk":223,"def":173,"sta":186},{"name":"Totodile","form":"Normal","atk":117,"def":109,"sta":137},{"name":"Croconaw","form":"Normal","atk":150,"def":142,"sta":163},{"name":"Feraligatr","form":"Normal","atk":205,"def":188,"sta":198},{"name":"Sentret","form":"Normal","atk":79,"def":73,"sta":111},{"name":"Furret","form":"Normal","atk":148,"def":125,"sta":198},{"name":"Hoothoot","form":"Normal","atk":67,"def":88,"sta":155},{"name":"Noctowl","form":"Normal","atk":145,"def":156,"sta":225},{"name":"Ledyba","form":"Normal","atk":72,"def":118,"sta":120},{"name":"Ledian","form":"Normal","atk":107,"def":179,"sta":146},{"name":"Spinarak","form":"Normal","atk":105,"def":73,"sta":120},{"name":"Ariados","form":"Normal","atk":161,"def":124,"sta":172},{"name":"Crobat","form":"Normal","atk":194,"def":178,"sta":198},{"name":"Chinchou","form":"Normal","atk":106,"def":97,"sta":181},{"name":"Lanturn","form":"Normal","atk":146,"def":137,"sta":268},{"name":"Pichu","form":"Normal","atk":77,"def":53,"sta":85},{"name":"Cleffa","form":"Normal","atk":75,"def":79,"sta":137},{"name":"Igglybuff","form":"Normal","atk":69,"def":32,"sta":207},{"name":"Togepi","form":"Normal","atk":67,"def":116,"sta":111},{"name":"Togetic","form":"Normal","atk":139,"def":181,"sta":146},{"name":"Natu","form":"Normal","atk":134,"def":89,"sta":120},{"name":"Xatu","form":"Normal","atk":192,"def":146,"sta":163},{"name":"Mareep","form":"Normal","atk":114,"def":79,"sta":146},{"name":"Flaaffy","form":"Normal","atk":145,"def":109,"sta":172},{"name":"Ampharos","form":"Normal","atk":211,"def":169,"sta":207},{"name":"Bellossom","form":"Normal","atk":169,"def":186,"sta":181},{"name":"Marill","form":"Normal","atk":37,"def":93,"sta":172},{"name":"Azumarill","form":"Normal","atk":112,"def":152,"sta":225},{"name":"Sudowoodo","form":"Normal","atk":167,"def":176,"sta":172},{"name":"Politoed","form":"Normal","atk":174,"def":179,"sta":207},{"name":"Hoppip","form":"Normal","atk":67,"def":94,"sta":111},{"name":"Skiploom","form":"Normal","atk":91,"def":120,"sta":146},{"name":"Jumpluff","form":"Normal","atk":118,"def":183,"sta":181},{"name":"Aipom","form":"Normal","atk":136,"def":112,"sta":146},{"name":"Sunkern","form":"Normal","atk":55,"def":55,"sta":102},{"name":"Sunflora","form":"Normal","atk":185,"def":135,"sta":181},{"name":"Yanma","form":"Normal","atk":154,"def":94,"sta":163},{"name":"Wooper","form":"Normal","atk":75,"def":66,"sta":146},{"name":"Wooper","form":"Paldea","atk":75,"def":66,"sta":146},{"name":"Quagsire","form":"Normal","atk":152,"def":143,"sta":216},{"name":"Espeon","form":"Gofest_2024_sscarf","atk":261,"def":175,"sta":163},{"name":"Espeon","form":"Normal","atk":261,"def":175,"sta":163},{"name":"Umbreon","form":"Gofest_2024_mscarf","atk":126,"def":240,"sta":216},{"name":"Umbreon","form":"Normal","atk":126,"def":240,"sta":216},{"name":"Murkrow","form":"Normal","atk":175,"def":87,"sta":155},{"name":"Slowking","form":"2022","atk":177,"def":180,"sta":216},{"name":"Slowking","form":"Galarian","atk":190,"def":180,"sta":216},{"name":"Slowking","form":"Normal","atk":177,"def":180,"sta":216},{"name":"Misdreavus","form":"Normal","atk":167,"def":154,"sta":155},{"name":"Unown","form":"A","atk":136,"def":91,"sta":134},{"name":"Unown","form":"B","atk":136,"def":91,"sta":134},{"name":"Unown","form":"C","atk":136,"def":91,"sta":134},{"name":"Unown","form":"D","atk":136,"def":91,"sta":134},{"name":"Unown","form":"E","atk":136,"def":91,"sta":134},{"name":"Unown","form":"Exclamation_point","atk":136,"def":91,"sta":134},{"name":"Unown","form":"F","atk":136,"def":91,"sta":134},{"name":"Unown","form":"G","atk":136,"def":91,"sta":134},{"name":"Unown","form":"H","atk":136,"def":91,"sta":134},{"name":"Unown","form":"I","atk":136,"def":91,"sta":134},{"name":"Unown","form":"J","atk":136,"def":91,"sta":134},{"name":"Unown","form":"K","atk":136,"def":91,"sta":134},{"name":"Unown","form":"L","atk":136,"def":91,"sta":134},{"name":"Unown","form":"M","atk":136,"def":91,"sta":134},{"name":"Unown","form":"N","atk":136,"def":91,"sta":134},{"name":"Unown","form":"O","atk":136,"def":91,"sta":134},{"name":"Unown","form":"P","atk":136,"def":91,"sta":134},{"name":"Unown","form":"Q","atk":136,"def":91,"sta":134},{"name":"Unown","form":"Question_mark","atk":136,"def":91,"sta":134},{"name":"Unown","form":"R","atk":136,"def":91,"sta":134},{"name":"Unown","form":"S","atk":136,"def":91,"sta":134},{"name":"Unown","form":"T","atk":136,"def":91,"sta":134},{"name":"Unown","form":"U","atk":136,"def":91,"sta":134},{"name":"Unown","form":"V","atk":136,"def":91,"sta":134},{"name":"Unown","form":"W","atk":136,"def":91,"sta":134},{"name":"Unown","form":"X","atk":136,"def":91,"sta":134},{"name":"Unown","form":"Y","atk":136,"def":91,"sta":134},{"name":"Unown","form":"Z","atk":136,"def":91,"sta":134},{"name":"Wobbuffet","form":"Normal","atk":60,"def":106,"sta":382},{"name":"Girafarig","form":"Normal","atk":182,"def":133,"sta":172},{"name":"Pineco","form":"Normal","atk":108,"def":122,"sta":137},{"name":"Forretress","form":"Normal","atk":161,"def":205,"sta":181},{"name":"Dunsparce","form":"Normal","atk":131,"def":128,"sta":225},{"name":"Gligar","form":"Normal","atk":143,"def":184,"sta":163},{"name":"Steelix","form":"Normal","atk":148,"def":272,"sta":181},{"name":"Snubbull","form":"Normal","atk":137,"def":85,"sta":155},{"name":"Granbull","form":"Normal","atk":212,"def":131,"sta":207},{"name":"Qwilfish","form":"Hisuian","atk":184,"def":151,"sta":163},{"name":"Qwilfish","form":"Normal","atk":184,"def":138,"sta":163},{"name":"Scizor","form":"Normal","atk":236,"def":181,"sta":172},{"name":"Shuckle","form":"Normal","atk":17,"def":396,"sta":85},{"name":"Heracross","form":"Normal","atk":234,"def":179,"sta":190},{"name":"Sneasel","form":"Hisuian","atk":189,"def":146,"sta":146},{"name":"Sneasel","form":"Normal","atk":189,"def":146,"sta":146},{"name":"Teddiursa","form":"Normal","atk":142,"def":93,"sta":155},{"name":"Ursaring","form":"Normal","atk":236,"def":144,"sta":207},{"name":"Slugma","form":"Normal","atk":118,"def":71,"sta":120},{"name":"Magcargo","form":"Normal","atk":139,"def":191,"sta":137},{"name":"Swinub","form":"Normal","atk":90,"def":69,"sta":137},{"name":"Piloswine","form":"Normal","atk":181,"def":138,"sta":225},{"name":"Corsola","form":"Galarian","atk":116,"def":182,"sta":155},{"name":"Corsola","form":"Normal","atk":118,"def":156,"sta":146},{"name":"Remoraid","form":"Normal","atk":127,"def":69,"sta":111},{"name":"Octillery","form":"Normal","atk":197,"def":141,"sta":181},{"name":"Delibird","form":"Normal","atk":128,"def":90,"sta":128},{"name":"Delibird","form":"Winter_2020","atk":128,"def":90,"sta":128},{"name":"Mantine","form":"Normal","atk":148,"def":226,"sta":163},{"name":"Skarmory","form":"Normal","atk":148,"def":226,"sta":163},{"name":"Houndour","form":"Normal","atk":152,"def":83,"sta":128},{"name":"Houndoom","form":"Normal","atk":224,"def":144,"sta":181},{"name":"Kingdra","form":"Normal","atk":194,"def":194,"sta":181},{"name":"Phanpy","form":"Normal","atk":107,"def":98,"sta":207},{"name":"Donphan","form":"Normal","atk":214,"def":185,"sta":207},{"name":"Porygon2","form":"Normal","atk":198,"def":180,"sta":198},{"name":"Stantler","form":"Normal","atk":192,"def":131,"sta":177},{"name":"Smeargle","form":"Normal","atk":40,"def":83,"sta":146},{"name":"Tyrogue","form":"Normal","atk":64,"def":64,"sta":111},{"name":"Hitmontop","form":"Normal","atk":173,"def":207,"sta":137},{"name":"Smoochum","form":"Normal","atk":153,"def":91,"sta":128},{"name":"Elekid","form":"Normal","atk":135,"def":101,"sta":128},{"name":"Magby","form":"Normal","atk":151,"def":99,"sta":128},{"name":"Miltank","form":"Normal","atk":157,"def":193,"sta":216},{"name":"Blissey","form":"Normal","atk":129,"def":169,"sta":496},{"name":"Raikou","form":"Normal","atk":241,"def":195,"sta":207},{"name":"Raikou","form":"S","atk":241,"def":195,"sta":207},{"name":"Entei","form":"Normal","atk":235,"def":171,"sta":251},{"name":"Entei","form":"S","atk":235,"def":171,"sta":251},{"name":"Suicune","form":"Normal","atk":180,"def":235,"sta":225},{"name":"Suicune","form":"S","atk":180,"def":235,"sta":225},{"name":"Larvitar","form":"Normal","atk":115,"def":93,"sta":137},{"name":"Pupitar","form":"Normal","atk":155,"def":133,"sta":172},{"name":"Tyranitar","form":"Normal","atk":251,"def":207,"sta":225},{"name":"Lugia","form":"Normal","atk":193,"def":310,"sta":235},{"name":"Lugia","form":"S","atk":193,"def":310,"sta":235},{"name":"Ho-Oh","form":"Normal","atk":239,"def":244,"sta":214},{"name":"Ho-Oh","form":"S","atk":239,"def":244,"sta":214},{"name":"Celebi","form":"Normal","atk":210,"def":210,"sta":225},{"name":"Treecko","form":"Normal","atk":124,"def":94,"sta":120},{"name":"Grovyle","form":"Normal","atk":172,"def":120,"sta":137},{"name":"Sceptile","form":"Normal","atk":223,"def":169,"sta":172},{"name":"Torchic","form":"Normal","atk":130,"def":87,"sta":128},{"name":"Combusken","form":"Normal","atk":163,"def":115,"sta":155},{"name":"Blaziken","form":"Normal","atk":240,"def":141,"sta":190},{"name":"Mudkip","form":"Normal","atk":126,"def":93,"sta":137},{"name":"Marshtomp","form":"Normal","atk":156,"def":133,"sta":172},{"name":"Swampert","form":"Normal","atk":208,"def":175,"sta":225},{"name":"Poochyena","form":"Normal","atk":96,"def":61,"sta":111},{"name":"Mightyena","form":"Normal","atk":171,"def":132,"sta":172},{"name":"Zigzagoon","form":"Galarian","atk":58,"def":80,"sta":116},{"name":"Zigzagoon","form":"Normal","atk":58,"def":80,"sta":116},{"name":"Linoone","form":"Galarian","atk":142,"def":128,"sta":186},{"name":"Linoone","form":"Normal","atk":142,"def":128,"sta":186},{"name":"Wurmple","form":"Normal","atk":75,"def":59,"sta":128},{"name":"Silcoon","form":"Normal","atk":60,"def":77,"sta":137},{"name":"Beautifly","form":"Normal","atk":189,"def":98,"sta":155},{"name":"Cascoon","form":"Normal","atk":60,"def":77,"sta":137},{"name":"Dustox","form":"Normal","atk":98,"def":162,"sta":155},{"name":"Lotad","form":"Normal","atk":71,"def":77,"sta":120},{"name":"Lombre","form":"Normal","atk":112,"def":119,"sta":155},{"name":"Ludicolo","form":"Normal","atk":173,"def":176,"sta":190},{"name":"Seedot","form":"Normal","atk":71,"def":77,"sta":120},{"name":"Nuzleaf","form":"Normal","atk":134,"def":78,"sta":172},{"name":"Shiftry","form":"Normal","atk":200,"def":121,"sta":207},{"name":"Taillow","form":"Normal","atk":106,"def":61,"sta":120},{"name":"Swellow","form":"Normal","atk":185,"def":124,"sta":155},{"name":"Wingull","form":"Normal","atk":106,"def":61,"sta":120},{"name":"Pelipper","form":"Normal","atk":175,"def":174,"sta":155},{"name":"Ralts","form":"Normal","atk":79,"def":59,"sta":99},{"name":"Kirlia","form":"Normal","atk":117,"def":90,"sta":116},{"name":"Gardevoir","form":"Normal","atk":237,"def":195,"sta":169},{"name":"Surskit","form":"Normal","atk":93,"def":87,"sta":120},{"name":"Masquerain","form":"Normal","atk":192,"def":150,"sta":172},{"name":"Shroomish","form":"Normal","atk":74,"def":110,"sta":155},{"name":"Breloom","form":"Normal","atk":241,"def":144,"sta":155},{"name":"Slakoth","form":"Normal","atk":104,"def":92,"sta":155},{"name":"Vigoroth","form":"Normal","atk":159,"def":145,"sta":190},{"name":"Slaking","form":"Normal","atk":290,"def":166,"sta":284},{"name":"Nincada","form":"Normal","atk":80,"def":126,"sta":104},{"name":"Ninjask","form":"Normal","atk":199,"def":112,"sta":156},{"name":"Shedinja","form":"Normal","atk":153,"def":73,"sta":1},{"name":"Whismur","form":"Normal","atk":92,"def":42,"sta":162},{"name":"Loudred","form":"Normal","atk":134,"def":81,"sta":197},{"name":"Exploud","form":"Normal","atk":179,"def":137,"sta":232},{"name":"Makuhita","form":"Normal","atk":99,"def":54,"sta":176},{"name":"Hariyama","form":"Normal","atk":209,"def":114,"sta":302},{"name":"Azurill","form":"Normal","atk":36,"def":71,"sta":137},{"name":"Nosepass","form":"Normal","atk":82,"def":215,"sta":102},{"name":"Skitty","form":"Normal","atk":84,"def":79,"sta":137},{"name":"Delcatty","form":"Normal","atk":132,"def":127,"sta":172},{"name":"Sableye","form":"Costume_2020","atk":141,"def":136,"sta":137},{"name":"Sableye","form":"Normal","atk":141,"def":136,"sta":137},{"name":"Mawile","form":"Normal","atk":155,"def":141,"sta":137},{"name":"Aron","form":"Normal","atk":121,"def":141,"sta":137},{"name":"Lairon","form":"Normal","atk":158,"def":198,"sta":155},{"name":"Aggron","form":"Normal","atk":198,"def":257,"sta":172},{"name":"Meditite","form":"Normal","atk":78,"def":107,"sta":102},{"name":"Medicham","form":"Normal","atk":121,"def":152,"sta":155},{"name":"Electrike","form":"Normal","atk":123,"def":78,"sta":120},{"name":"Manectric","form":"Normal","atk":215,"def":127,"sta":172},{"name":"Plusle","form":"Normal","atk":167,"def":129,"sta":155},{"name":"Minun","form":"Normal","atk":147,"def":150,"sta":155},{"name":"Volbeat","form":"Normal","atk":143,"def":166,"sta":163},{"name":"Illumise","form":"Normal","atk":143,"def":166,"sta":163},{"name":"Roselia","form":"Normal","atk":186,"def":131,"sta":137},{"name":"Gulpin","form":"Normal","atk":80,"def":99,"sta":172},{"name":"Swalot","form":"Normal","atk":140,"def":159,"sta":225},{"name":"Carvanha","form":"Normal","atk":171,"def":39,"sta":128},{"name":"Sharpedo","form":"Normal","atk":243,"def":83,"sta":172},{"name":"Wailmer","form":"Normal","atk":136,"def":68,"sta":277},{"name":"Wailord","form":"Normal","atk":175,"def":87,"sta":347},{"name":"Numel","form":"Normal","atk":119,"def":79,"sta":155},{"name":"Camerupt","form":"Normal","atk":194,"def":136,"sta":172},{"name":"Torkoal","form":"Normal","atk":151,"def":203,"sta":172},{"name":"Spoink","form":"Normal","atk":125,"def":122,"sta":155},{"name":"Grumpig","form":"Normal","atk":171,"def":188,"sta":190},{"name":"Spinda","form":"00","atk":116,"def":116,"sta":155},{"name":"Spinda","form":"01","atk":116,"def":116,"sta":155},{"name":"Spinda","form":"02","atk":116,"def":116,"sta":155},{"name":"Spinda","form":"03","atk":116,"def":116,"sta":155},{"name":"Spinda","form":"04","atk":116,"def":116,"sta":155},{"name":"Spinda","form":"05","atk":116,"def":116,"sta":155},{"name":"Spinda","form":"06","atk":116,"def":116,"sta":155},{"name":"Spinda","form":"07","atk":116,"def":116,"sta":155},{"name":"Spinda","form":"08","atk":116,"def":116,"sta":155},{"name":"Spinda","form":"09","atk":116,"def":116,"sta":155},{"name":"Spinda","form":"10","atk":116,"def":116,"sta":155},{"name":"Spinda","form":"11","atk":116,"def":116,"sta":155},{"name":"Spinda","form":"12","atk":116,"def":116,"sta":155},{"name":"Spinda","form":"13","atk":116,"def":116,"sta":155},{"name":"Spinda","form":"14","atk":116,"def":116,"sta":155},{"name":"Spinda","form":"15","atk":116,"def":116,"sta":155},{"name":"Spinda","form":"16","atk":116,"def":116,"sta":155},{"name":"Spinda","form":"17","atk":116,"def":116,"sta":155},{"name":"Spinda","form":"18","atk":116,"def":116,"sta":155},{"name":"Spinda","form":"19","atk":116,"def":116,"sta":155},{"name":"Trapinch","form":"Normal","atk":162,"def":78,"sta":128},{"name":"Vibrava","form":"Normal","atk":134,"def":99,"sta":137},{"name":"Flygon","form":"Normal","atk":205,"def":168,"sta":190},{"name":"Cacnea","form":"Normal","atk":156,"def":74,"sta":137},{"name":"Cacturne","form":"Normal","atk":221,"def":115,"sta":172},{"name":"Swablu","form":"Normal","atk":76,"def":132,"sta":128},{"name":"Altaria","form":"Normal","atk":141,"def":201,"sta":181},{"name":"Zangoose","form":"Normal","atk":222,"def":124,"sta":177},{"name":"Seviper","form":"Normal","atk":196,"def":118,"sta":177},{"name":"Lunatone","form":"Normal","atk":178,"def":153,"sta":207},{"name":"Solrock","form":"Normal","atk":178,"def":153,"sta":207},{"name":"Barboach","form":"Normal","atk":93,"def":82,"sta":137},{"name":"Whiscash","form":"Normal","atk":151,"def":141,"sta":242},{"name":"Corphish","form":"Normal","atk":141,"def":99,"sta":125},{"name":"Crawdaunt","form":"Normal","atk":224,"def":142,"sta":160},{"name":"Baltoy","form":"Normal","atk":77,"def":124,"sta":120},{"name":"Claydol","form":"Normal","atk":140,"def":229,"sta":155},{"name":"Lileep","form":"Normal","atk":105,"def":150,"sta":165},{"name":"Cradily","form":"Normal","atk":152,"def":194,"sta":200},{"name":"Anorith","form":"Normal","atk":176,"def":100,"sta":128},{"name":"Armaldo","form":"Normal","atk":222,"def":174,"sta":181},{"name":"Feebas","form":"Normal","atk":29,"def":85,"sta":85},{"name":"Milotic","form":"Normal","atk":192,"def":219,"sta":216},{"name":"Castform","form":"Normal","atk":139,"def":139,"sta":172},{"name":"Castform","form":"Rainy","atk":139,"def":139,"sta":172},{"name":"Castform","form":"Snowy","atk":139,"def":139,"sta":172},{"name":"Castform","form":"Sunny","atk":139,"def":139,"sta":172},{"name":"Kecleon","form":"Normal","atk":161,"def":189,"sta":155},{"name":"Shuppet","form":"Normal","atk":138,"def":65,"sta":127},{"name":"Banette","form":"Normal","atk":218,"def":126,"sta":162},{"name":"Duskull","form":"Normal","atk":70,"def":162,"sta":85},{"name":"Dusclops","form":"Normal","atk":124,"def":234,"sta":120},{"name":"Tropius","form":"Normal","atk":136,"def":163,"sta":223},{"name":"Chimecho","form":"Normal","atk":175,"def":170,"sta":181},{"name":"Absol","form":"Normal","atk":246,"def":120,"sta":163},{"name":"Wynaut","form":"Normal","atk":41,"def":86,"sta":216},{"name":"Snorunt","form":"Normal","atk":95,"def":95,"sta":137},{"name":"Glalie","form":"Normal","atk":162,"def":162,"sta":190},{"name":"Spheal","form":"Normal","atk":95,"def":90,"sta":172},{"name":"Sealeo","form":"Normal","atk":137,"def":132,"sta":207},{"name":"Walrein","form":"Normal","atk":182,"def":176,"sta":242},{"name":"Clamperl","form":"Normal","atk":133,"def":135,"sta":111},{"name":"Huntail","form":"Normal","atk":197,"def":179,"sta":146},{"name":"Gorebyss","form":"Normal","atk":211,"def":179,"sta":146},{"name":"Relicanth","form":"Normal","atk":162,"def":203,"sta":225},{"name":"Luvdisc","form":"Normal","atk":81,"def":128,"sta":125},{"name":"Bagon","form":"Normal","atk":134,"def":93,"sta":128},{"name":"Shelgon","form":"Normal","atk":172,"def":155,"sta":163},{"name":"Salamence","form":"Normal","atk":277,"def":168,"sta":216},{"name":"Beldum","form":"Normal","atk":96,"def":132,"sta":120},{"name":"Metang","form":"Normal","atk":138,"def":176,"sta":155},{"name":"Metagross","form":"Normal","atk":257,"def":228,"sta":190},{"name":"Regirock","form":"Normal","atk":179,"def":309,"sta":190},{"name":"Regice","form":"Normal","atk":179,"def":309,"sta":190},{"name":"Registeel","form":"Normal","atk":143,"def":285,"sta":190},{"name":"Latias","form":"Normal","atk":228,"def":246,"sta":190},{"name":"Latias","form":"S","atk":228,"def":246,"sta":190},{"name":"Latios","form":"Normal","atk":268,"def":212,"sta":190},{"name":"Latios","form":"S","atk":268,"def":212,"sta":190},{"name":"Kyogre","form":"Normal","atk":270,"def":228,"sta":205},{"name":"Groudon","form":"Normal","atk":270,"def":228,"sta":205},{"name":"Rayquaza","form":"Normal","atk":284,"def":170,"sta":213},{"name":"Jirachi","form":"Normal","atk":210,"def":210,"sta":225},{"name":"Deoxys","form":"Attack","atk":414,"def":46,"sta":137},{"name":"Deoxys","form":"Defense","atk":144,"def":330,"sta":137},{"name":"Deoxys","form":"Normal","atk":345,"def":115,"sta":137},{"name":"Deoxys","form":"Speed","atk":230,"def":218,"sta":137},{"name":"Turtwig","form":"Normal","atk":119,"def":110,"sta":146},{"name":"Grotle","form":"Normal","atk":157,"def":143,"sta":181},{"name":"Torterra","form":"Normal","atk":202,"def":188,"sta":216},{"name":"Chimchar","form":"Normal","atk":113,"def":86,"sta":127},{"name":"Monferno","form":"Normal","atk":158,"def":105,"sta":162},{"name":"Infernape","form":"Normal","atk":222,"def":151,"sta":183},{"name":"Piplup","form":"Normal","atk":112,"def":102,"sta":142},{"name":"Prinplup","form":"Normal","atk":150,"def":139,"sta":162},{"name":"Empoleon","form":"Normal","atk":210,"def":186,"sta":197},{"name":"Starly","form":"Normal","atk":101,"def":58,"sta":120},{"name":"Staravia","form":"Normal","atk":142,"def":94,"sta":146},{"name":"Staraptor","form":"Normal","atk":234,"def":140,"sta":198},{"name":"Bidoof","form":"Normal","atk":80,"def":73,"sta":153},{"name":"Bibarel","form":"Normal","atk":162,"def":119,"sta":188},{"name":"Kricketot","form":"Normal","atk":45,"def":74,"sta":114},{"name":"Kricketune","form":"Normal","atk":160,"def":100,"sta":184},{"name":"Shinx","form":"Normal","atk":117,"def":64,"sta":128},{"name":"Luxio","form":"Normal","atk":159,"def":95,"sta":155},{"name":"Luxray","form":"Normal","atk":232,"def":156,"sta":190},{"name":"Budew","form":"Normal","atk":91,"def":109,"sta":120},{"name":"Roserade","form":"Normal","atk":243,"def":185,"sta":155},{"name":"Cranidos","form":"Normal","atk":218,"def":71,"sta":167},{"name":"Rampardos","form":"Normal","atk":295,"def":109,"sta":219},{"name":"Shieldon","form":"Normal","atk":76,"def":195,"sta":102},{"name":"Bastiodon","form":"Normal","atk":94,"def":286,"sta":155},{"name":"Burmy","form":"Plant","atk":53,"def":83,"sta":120},{"name":"Burmy","form":"Sandy","atk":53,"def":83,"sta":120},{"name":"Burmy","form":"Trash","atk":53,"def":83,"sta":120},{"name":"Wormadam","form":"Plant","atk":141,"def":180,"sta":155},{"name":"Wormadam","form":"Sandy","atk":141,"def":180,"sta":155},{"name":"Wormadam","form":"Trash","atk":127,"def":175,"sta":155},{"name":"Mothim","form":"Normal","atk":185,"def":98,"sta":172},{"name":"Combee","form":"Normal","atk":59,"def":83,"sta":102},{"name":"Vespiquen","form":"Normal","atk":149,"def":190,"sta":172},{"name":"Pachirisu","form":"Normal","atk":94,"def":172,"sta":155},{"name":"Buizel","form":"Normal","atk":132,"def":67,"sta":146},{"name":"Floatzel","form":"Normal","atk":221,"def":114,"sta":198},{"name":"Cherubi","form":"Normal","atk":108,"def":92,"sta":128},{"name":"Cherrim","form":"Overcast","atk":170,"def":153,"sta":172},{"name":"Cherrim","form":"Sunny","atk":170,"def":153,"sta":172},{"name":"Shellos","form":"East_sea","atk":103,"def":105,"sta":183},{"name":"Shellos","form":"West_sea","atk":103,"def":105,"sta":183},{"name":"Gastrodon","form":"East_sea","atk":169,"def":143,"sta":244},{"name":"Gastrodon","form":"West_sea","atk":169,"def":143,"sta":244},{"name":"Ambipom","form":"Normal","atk":205,"def":143,"sta":181},{"name":"Drifloon","form":"Normal","atk":117,"def":80,"sta":207},{"name":"Drifblim","form":"Normal","atk":180,"def":102,"sta":312},{"name":"Buneary","form":"Normal","atk":130,"def":105,"sta":146},{"name":"Lopunny","form":"Normal","atk":156,"def":194,"sta":163},{"name":"Mismagius","form":"Normal","atk":211,"def":187,"sta":155},{"name":"Honchkrow","form":"Normal","atk":243,"def":103,"sta":225},{"name":"Glameow","form":"Normal","atk":109,"def":82,"sta":135},{"name":"Purugly","form":"Normal","atk":172,"def":133,"sta":174},{"name":"Chingling","form":"Normal","atk":114,"def":94,"sta":128},{"name":"Stunky","form":"Normal","atk":121,"def":90,"sta":160},{"name":"Skuntank","form":"Normal","atk":184,"def":132,"sta":230},{"name":"Bronzor","form":"Normal","atk":43,"def":154,"sta":149},{"name":"Bronzong","form":"Normal","atk":161,"def":213,"sta":167},{"name":"Bonsly","form":"Normal","atk":124,"def":133,"sta":137},{"name":"Mime Jr.","form":"Normal","atk":125,"def":142,"sta":85},{"name":"Happiny","form":"Normal","atk":25,"def":77,"sta":225},{"name":"Chatot","form":"Normal","atk":183,"def":91,"sta":183},{"name":"Spiritomb","form":"Normal","atk":169,"def":199,"sta":137},{"name":"Gible","form":"Normal","atk":124,"def":84,"sta":151},{"name":"Gabite","form":"Normal","atk":172,"def":125,"sta":169},{"name":"Garchomp","form":"Normal","atk":261,"def":193,"sta":239},{"name":"Munchlax","form":"Normal","atk":137,"def":117,"sta":286},{"name":"Riolu","form":"Normal","atk":127,"def":78,"sta":120},{"name":"Lucario","form":"Normal","atk":236,"def":144,"sta":172},{"name":"Hippopotas","form":"Normal","atk":124,"def":118,"sta":169},{"name":"Hippowdon","form":"Normal","atk":201,"def":191,"sta":239},{"name":"Skorupi","form":"Normal","atk":93,"def":151,"sta":120},{"name":"Drapion","form":"Normal","atk":180,"def":202,"sta":172},{"name":"Croagunk","form":"Normal","atk":116,"def":76,"sta":134},{"name":"Toxicroak","form":"Normal","atk":211,"def":133,"sta":195},{"name":"Carnivine","form":"Normal","atk":187,"def":136,"sta":179},{"name":"Finneon","form":"Normal","atk":96,"def":116,"sta":135},{"name":"Lumineon","form":"Normal","atk":142,"def":170,"sta":170},{"name":"Mantyke","form":"Normal","atk":105,"def":179,"sta":128},{"name":"Snover","form":"Normal","atk":115,"def":105,"sta":155},{"name":"Abomasnow","form":"Normal","atk":178,"def":158,"sta":207},{"name":"Weavile","form":"Normal","atk":243,"def":171,"sta":172},{"name":"Magnezone","form":"Normal","atk":238,"def":205,"sta":172},{"name":"Lickilicky","form":"Normal","atk":161,"def":181,"sta":242},{"name":"Rhyperior","form":"Normal","atk":241,"def":190,"sta":251},{"name":"Tangrowth","form":"Normal","atk":207,"def":184,"sta":225},{"name":"Electivire","form":"Normal","atk":249,"def":163,"sta":181},{"name":"Magmortar","form":"Normal","atk":247,"def":172,"sta":181},{"name":"Togekiss","form":"Normal","atk":225,"def":217,"sta":198},{"name":"Yanmega","form":"Normal","atk":231,"def":156,"sta":200},{"name":"Leafeon","form":"Normal","atk":216,"def":219,"sta":163},{"name":"Glaceon","form":"Normal","atk":238,"def":205,"sta":163},{"name":"Gliscor","form":"Normal","atk":185,"def":222,"sta":181},{"name":"Mamoswine","form":"Normal","atk":247,"def":146,"sta":242},{"name":"Porygon-Z","form":"Normal","atk":264,"def":150,"sta":198},{"name":"Gallade","form":"Normal","atk":237,"def":195,"sta":169},{"name":"Probopass","form":"Normal","atk":135,"def":275,"sta":155},{"name":"Dusknoir","form":"Normal","atk":180,"def":254,"sta":128},{"name":"Froslass","form":"Normal","atk":171,"def":150,"sta":172},{"name":"Rotom","form":"Fan","atk":204,"def":219,"sta":137},{"name":"Rotom","form":"Frost","atk":204,"def":219,"sta":137},{"name":"Rotom","form":"Heat","atk":204,"def":219,"sta":137},{"name":"Rotom","form":"Mow","atk":204,"def":219,"sta":137},{"name":"Rotom","form":"Normal","atk":185,"def":159,"sta":137},{"name":"Rotom","form":"Wash","atk":204,"def":219,"sta":137},{"name":"Uxie","form":"Normal","atk":156,"def":270,"sta":181},{"name":"Mesprit","form":"Normal","atk":212,"def":212,"sta":190},{"name":"Azelf","form":"Normal","atk":270,"def":151,"sta":181},{"name":"Dialga","form":"Normal","atk":275,"def":211,"sta":205},{"name":"Dialga","form":"Origin","atk":270,"def":225,"sta":205},{"name":"Palkia","form":"Normal","atk":280,"def":215,"sta":189},{"name":"Palkia","form":"Origin","atk":286,"def":223,"sta":189},{"name":"Heatran","form":"Normal","atk":251,"def":213,"sta":209},{"name":"Regigigas","form":"Normal","atk":287,"def":210,"sta":221},{"name":"Giratina","form":"Altered","atk":187,"def":225,"sta":284},{"name":"Giratina","form":"Origin","atk":225,"def":187,"sta":284},{"name":"Cresselia","form":"Normal","atk":152,"def":258,"sta":260},{"name":"Phione","form":"Normal","atk":162,"def":162,"sta":190},{"name":"Manaphy","form":"Normal","atk":210,"def":210,"sta":225},{"name":"Darkrai","form":"Normal","atk":285,"def":198,"sta":172},{"name":"Shaymin","form":"Land","atk":210,"def":210,"sta":225},{"name":"Shaymin","form":"Sky","atk":261,"def":166,"sta":225},{"name":"Arceus","form":"Bug","atk":238,"def":238,"sta":237},{"name":"Arceus","form":"Dark","atk":238,"def":238,"sta":237},{"name":"Arceus","form":"Dragon","atk":238,"def":238,"sta":237},{"name":"Arceus","form":"Electric","atk":238,"def":238,"sta":237},{"name":"Arceus","form":"Fairy","atk":238,"def":238,"sta":237},{"name":"Arceus","form":"Fighting","atk":238,"def":238,"sta":237},{"name":"Arceus","form":"Fire","atk":238,"def":238,"sta":237},{"name":"Arceus","form":"Flying","atk":238,"def":238,"sta":237},{"name":"Arceus","form":"Ghost","atk":238,"def":238,"sta":237},{"name":"Arceus","form":"Grass","atk":238,"def":238,"sta":237},{"name":"Arceus","form":"Ground","atk":238,"def":238,"sta":237},{"name":"Arceus","form":"Ice","atk":238,"def":238,"sta":237},{"name":"Arceus","form":"Normal","atk":238,"def":238,"sta":237},{"name":"Arceus","form":"Poison","atk":238,"def":238,"sta":237},{"name":"Arceus","form":"Psychic","atk":238,"def":238,"sta":237},{"name":"Arceus","form":"Rock","atk":238,"def":238,"sta":237},{"name":"Arceus","form":"Steel","atk":238,"def":238,"sta":237},{"name":"Arceus","form":"Water","atk":238,"def":238,"sta":237},{"name":"Victini","form":"Normal","atk":210,"def":210,"sta":225},{"name":"Snivy","form":"Normal","atk":88,"def":107,"sta":128},{"name":"Servine","form":"Normal","atk":122,"def":152,"sta":155},{"name":"Serperior","form":"Normal","atk":161,"def":204,"sta":181},{"name":"Tepig","form":"Normal","atk":115,"def":85,"sta":163},{"name":"Pignite","form":"Normal","atk":173,"def":106,"sta":207},{"name":"Emboar","form":"Normal","atk":235,"def":127,"sta":242},{"name":"Oshawott","form":"Normal","atk":117,"def":85,"sta":146},{"name":"Dewott","form":"Normal","atk":159,"def":116,"sta":181},{"name":"Samurott","form":"Hisuian","atk":218,"def":152,"sta":207},{"name":"Samurott","form":"Normal","atk":212,"def":157,"sta":216},{"name":"Patrat","form":"Normal","atk":98,"def":73,"sta":128},{"name":"Watchog","form":"Normal","atk":165,"def":139,"sta":155},{"name":"Lillipup","form":"Normal","atk":107,"def":86,"sta":128},{"name":"Herdier","form":"Normal","atk":145,"def":126,"sta":163},{"name":"Stoutland","form":"Normal","atk":206,"def":182,"sta":198},{"name":"Purrloin","form":"Normal","atk":98,"def":73,"sta":121},{"name":"Liepard","form":"Normal","atk":187,"def":106,"sta":162},{"name":"Pansage","form":"Normal","atk":104,"def":94,"sta":137},{"name":"Simisage","form":"Normal","atk":206,"def":133,"sta":181},{"name":"Pansear","form":"Normal","atk":104,"def":94,"sta":137},{"name":"Simisear","form":"Normal","atk":206,"def":133,"sta":181},{"name":"Panpour","form":"Normal","atk":104,"def":94,"sta":137},{"name":"Simipour","form":"Normal","atk":206,"def":133,"sta":181},{"name":"Munna","form":"Normal","atk":111,"def":92,"sta":183},{"name":"Musharna","form":"Normal","atk":183,"def":166,"sta":253},{"name":"Pidove","form":"Normal","atk":98,"def":80,"sta":137},{"name":"Tranquill","form":"Normal","atk":144,"def":107,"sta":158},{"name":"Unfezant","form":"Normal","atk":226,"def":146,"sta":190},{"name":"Blitzle","form":"Normal","atk":118,"def":64,"sta":128},{"name":"Zebstrika","form":"Normal","atk":211,"def":136,"sta":181},{"name":"Roggenrola","form":"Normal","atk":121,"def":110,"sta":146},{"name":"Boldore","form":"Normal","atk":174,"def":143,"sta":172},{"name":"Gigalith","form":"Normal","atk":226,"def":201,"sta":198},{"name":"Woobat","form":"Normal","atk":107,"def":85,"sta":163},{"name":"Swoobat","form":"Normal","atk":161,"def":119,"sta":167},{"name":"Drilbur","form":"Normal","atk":154,"def":85,"sta":155},{"name":"Excadrill","form":"Normal","atk":255,"def":129,"sta":242},{"name":"Audino","form":"Normal","atk":114,"def":163,"sta":230},{"name":"Timburr","form":"Normal","atk":134,"def":87,"sta":181},{"name":"Gurdurr","form":"Normal","atk":180,"def":134,"sta":198},{"name":"Conkeldurr","form":"Normal","atk":243,"def":158,"sta":233},{"name":"Tympole","form":"Normal","atk":98,"def":78,"sta":137},{"name":"Palpitoad","form":"Normal","atk":128,"def":109,"sta":181},{"name":"Seismitoad","form":"Normal","atk":188,"def":150,"sta":233},{"name":"Throh","form":"Normal","atk":172,"def":160,"sta":260},{"name":"Sawk","form":"Normal","atk":231,"def":153,"sta":181},{"name":"Sewaddle","form":"Normal","atk":96,"def":124,"sta":128},{"name":"Swadloon","form":"Normal","atk":115,"def":162,"sta":146},{"name":"Leavanny","form":"Normal","atk":205,"def":165,"sta":181},{"name":"Venipede","form":"Normal","atk":83,"def":99,"sta":102},{"name":"Whirlipede","form":"Normal","atk":100,"def":173,"sta":120},{"name":"Scolipede","form":"Normal","atk":203,"def":175,"sta":155},{"name":"Cottonee","form":"Normal","atk":71,"def":111,"sta":120},{"name":"Whimsicott","form":"Normal","atk":164,"def":176,"sta":155},{"name":"Petilil","form":"Normal","atk":119,"def":91,"sta":128},{"name":"Lilligant","form":"Hisuian","atk":208,"def":159,"sta":172},{"name":"Lilligant","form":"Normal","atk":214,"def":155,"sta":172},{"name":"Basculin","form":"Blue_striped","atk":189,"def":129,"sta":172},{"name":"Basculin","form":"Red_striped","atk":189,"def":129,"sta":172},{"name":"Basculin","form":"White_striped","atk":189,"def":129,"sta":172},{"name":"Sandile","form":"Normal","atk":132,"def":69,"sta":137},{"name":"Krokorok","form":"Normal","atk":155,"def":90,"sta":155},{"name":"Krookodile","form":"Normal","atk":229,"def":158,"sta":216},{"name":"Darumaka","form":"Galarian","atk":153,"def":86,"sta":172},{"name":"Darumaka","form":"Normal","atk":153,"def":86,"sta":172},{"name":"Darmanitan","form":"Galarian_standard","atk":263,"def":114,"sta":233},{"name":"Darmanitan","form":"Galarian_zen","atk":323,"def":123,"sta":233},{"name":"Darmanitan","form":"Standard","atk":263,"def":114,"sta":233},{"name":"Darmanitan","form":"Zen","atk":243,"def":202,"sta":233},{"name":"Maractus","form":"Normal","atk":201,"def":130,"sta":181},{"name":"Dwebble","form":"Normal","atk":118,"def":128,"sta":137},{"name":"Crustle","form":"Normal","atk":188,"def":200,"sta":172},{"name":"Scraggy","form":"Normal","atk":132,"def":132,"sta":137},{"name":"Scrafty","form":"Normal","atk":163,"def":222,"sta":163},{"name":"Sigilyph","form":"Normal","atk":204,"def":167,"sta":176},{"name":"Yamask","form":"Galarian","atk":95,"def":141,"sta":116},{"name":"Yamask","form":"Normal","atk":95,"def":141,"sta":116},{"name":"Cofagrigus","form":"Normal","atk":163,"def":237,"sta":151},{"name":"Tirtouga","form":"Normal","atk":134,"def":146,"sta":144},{"name":"Carracosta","form":"Normal","atk":192,"def":197,"sta":179},{"name":"Archen","form":"Normal","atk":213,"def":89,"sta":146},{"name":"Archeops","form":"Normal","atk":292,"def":139,"sta":181},{"name":"Trubbish","form":"Normal","atk":96,"def":122,"sta":137},{"name":"Garbodor","form":"Normal","atk":181,"def":164,"sta":190},{"name":"Zorua","form":"Hisuian","atk":162,"def":79,"sta":111},{"name":"Zorua","form":"Normal","atk":153,"def":78,"sta":120},{"name":"Zoroark","form":"Hisuian","atk":261,"def":128,"sta":146},{"name":"Zoroark","form":"Normal","atk":250,"def":127,"sta":155},{"name":"Minccino","form":"Normal","atk":98,"def":80,"sta":146},{"name":"Cinccino","form":"Normal","atk":198,"def":130,"sta":181},{"name":"Gothita","form":"Normal","atk":98,"def":112,"sta":128},{"name":"Gothorita","form":"Normal","atk":137,"def":153,"sta":155},{"name":"Gothitelle","form":"Normal","atk":176,"def":205,"sta":172},{"name":"Solosis","form":"Normal","atk":170,"def":83,"sta":128},{"name":"Duosion","form":"Normal","atk":208,"def":103,"sta":163},{"name":"Reuniclus","form":"Normal","atk":214,"def":148,"sta":242},{"name":"Ducklett","form":"Normal","atk":84,"def":96,"sta":158},{"name":"Swanna","form":"Normal","atk":182,"def":132,"sta":181},{"name":"Vanillite","form":"Normal","atk":118,"def":106,"sta":113},{"name":"Vanillish","form":"Normal","atk":151,"def":138,"sta":139},{"name":"Vanilluxe","form":"Normal","atk":218,"def":184,"sta":174},{"name":"Deerling","form":"Autumn","atk":115,"def":100,"sta":155},{"name":"Deerling","form":"Spring","atk":115,"def":100,"sta":155},{"name":"Deerling","form":"Summer","atk":115,"def":100,"sta":155},{"name":"Deerling","form":"Winter","atk":115,"def":100,"sta":155},{"name":"Sawsbuck","form":"Autumn","atk":198,"def":146,"sta":190},{"name":"Sawsbuck","form":"Spring","atk":198,"def":146,"sta":190},{"name":"Sawsbuck","form":"Summer","atk":198,"def":146,"sta":190},{"name":"Sawsbuck","form":"Winter","atk":198,"def":146,"sta":190},{"name":"Emolga","form":"Normal","atk":158,"def":127,"sta":146},{"name":"Karrablast","form":"Normal","atk":137,"def":87,"sta":137},{"name":"Escavalier","form":"Normal","atk":223,"def":187,"sta":172},{"name":"Foongus","form":"Normal","atk":97,"def":91,"sta":170},{"name":"Amoonguss","form":"Normal","atk":155,"def":139,"sta":249},{"name":"Frillish","form":"Female","atk":115,"def":134,"sta":146},{"name":"Frillish","form":"Normal","atk":115,"def":134,"sta":146},{"name":"Jellicent","form":"Female","atk":159,"def":178,"sta":225},{"name":"Jellicent","form":"Normal","atk":159,"def":178,"sta":225},{"name":"Alomomola","form":"Normal","atk":138,"def":131,"sta":338},{"name":"Joltik","form":"Normal","atk":110,"def":98,"sta":137},{"name":"Galvantula","form":"Normal","atk":201,"def":128,"sta":172},{"name":"Ferroseed","form":"Normal","atk":82,"def":155,"sta":127},{"name":"Ferrothorn","form":"Normal","atk":158,"def":223,"sta":179},{"name":"Klink","form":"Normal","atk":98,"def":121,"sta":120},{"name":"Klang","form":"Normal","atk":150,"def":174,"sta":155},{"name":"Klinklang","form":"Normal","atk":199,"def":214,"sta":155},{"name":"Tynamo","form":"Normal","atk":105,"def":78,"sta":111},{"name":"Eelektrik","form":"Normal","atk":156,"def":130,"sta":163},{"name":"Eelektross","form":"Normal","atk":217,"def":152,"sta":198},{"name":"Elgyem","form":"Normal","atk":148,"def":100,"sta":146},{"name":"Beheeyem","form":"Normal","atk":221,"def":163,"sta":181},{"name":"Litwick","form":"Normal","atk":108,"def":98,"sta":137},{"name":"Lampent","form":"Normal","atk":169,"def":115,"sta":155},{"name":"Chandelure","form":"Normal","atk":271,"def":182,"sta":155},{"name":"Axew","form":"Normal","atk":154,"def":101,"sta":130},{"name":"Fraxure","form":"Normal","atk":212,"def":123,"sta":165},{"name":"Haxorus","form":"Normal","atk":284,"def":172,"sta":183},{"name":"Cubchoo","form":"Normal","atk":128,"def":74,"sta":146},{"name":"Cubchoo","form":"Winter_2020","atk":128,"def":74,"sta":146},{"name":"Beartic","form":"Normal","atk":233,"def":152,"sta":216},{"name":"Beartic","form":"Winter_2020","atk":233,"def":152,"sta":216},{"name":"Cryogonal","form":"Normal","atk":190,"def":218,"sta":190},{"name":"Shelmet","form":"Normal","atk":72,"def":140,"sta":137},{"name":"Accelgor","form":"Normal","atk":220,"def":120,"sta":190},{"name":"Stunfisk","form":"Galarian","atk":144,"def":171,"sta":240},{"name":"Stunfisk","form":"Normal","atk":144,"def":171,"sta":240},{"name":"Mienfoo","form":"Normal","atk":160,"def":98,"sta":128},{"name":"Mienshao","form":"Normal","atk":258,"def":127,"sta":163},{"name":"Druddigon","form":"Normal","atk":213,"def":170,"sta":184},{"name":"Golett","form":"Normal","atk":127,"def":92,"sta":153},{"name":"Golurk","form":"Normal","atk":222,"def":154,"sta":205},{"name":"Pawniard","form":"Normal","atk":154,"def":114,"sta":128},{"name":"Bisharp","form":"Normal","atk":232,"def":176,"sta":163},{"name":"Bouffalant","form":"Normal","atk":195,"def":182,"sta":216},{"name":"Rufflet","form":"Normal","atk":150,"def":97,"sta":172},{"name":"Braviary","form":"Hisuian","atk":213,"def":137,"sta":242},{"name":"Braviary","form":"Normal","atk":232,"def":152,"sta":225},{"name":"Vullaby","form":"Normal","atk":105,"def":139,"sta":172},{"name":"Mandibuzz","form":"Normal","atk":129,"def":205,"sta":242},{"name":"Heatmor","form":"Normal","atk":204,"def":129,"sta":198},{"name":"Durant","form":"Normal","atk":217,"def":188,"sta":151},{"name":"Deino","form":"Normal","atk":116,"def":93,"sta":141},{"name":"Zweilous","form":"Normal","atk":159,"def":135,"sta":176},{"name":"Hydreigon","form":"Normal","atk":256,"def":188,"sta":211},{"name":"Larvesta","form":"Normal","atk":156,"def":107,"sta":146},{"name":"Volcarona","form":"Normal","atk":264,"def":189,"sta":198},{"name":"Cobalion","form":"Normal","atk":192,"def":229,"sta":209},{"name":"Terrakion","form":"Normal","atk":260,"def":192,"sta":209},{"name":"Virizion","form":"Normal","atk":192,"def":229,"sta":209},{"name":"Tornadus","form":"Incarnate","atk":266,"def":164,"sta":188},{"name":"Tornadus","form":"Therian","atk":238,"def":189,"sta":188},{"name":"Thundurus","form":"Incarnate","atk":266,"def":164,"sta":188},{"name":"Thundurus","form":"Therian","atk":295,"def":161,"sta":188},{"name":"Reshiram","form":"Normal","atk":275,"def":211,"sta":205},{"name":"Zekrom","form":"Normal","atk":275,"def":211,"sta":205},{"name":"Landorus","form":"Incarnate","atk":261,"def":182,"sta":205},{"name":"Landorus","form":"Therian","atk":289,"def":179,"sta":205},{"name":"Kyurem","form":"Black","atk":310,"def":183,"sta":245},{"name":"Kyurem","form":"Normal","atk":246,"def":170,"sta":245},{"name":"Kyurem","form":"White","atk":310,"def":183,"sta":245},{"name":"Keldeo","form":"Ordinary","atk":260,"def":192,"sta":209},{"name":"Keldeo","form":"Resolute","atk":260,"def":192,"sta":209},{"name":"Meloetta","form":"Aria","atk":250,"def":225,"sta":225},{"name":"Meloetta","form":"Pirouette","atk":269,"def":188,"sta":225},{"name":"Genesect","form":"Burn","atk":252,"def":199,"sta":174},{"name":"Genesect","form":"Chill","atk":252,"def":199,"sta":174},{"name":"Genesect","form":"Douse","atk":252,"def":199,"sta":174},{"name":"Genesect","form":"Normal","atk":252,"def":199,"sta":174},{"name":"Genesect","form":"Shock","atk":252,"def":199,"sta":174},{"name":"Chespin","form":"Normal","atk":110,"def":106,"sta":148},{"name":"Quilladin","form":"Normal","atk":146,"def":156,"sta":156},{"name":"Chesnaught","form":"Normal","atk":201,"def":204,"sta":204},{"name":"Fennekin","form":"Normal","atk":116,"def":102,"sta":120},{"name":"Braixen","form":"Normal","atk":171,"def":130,"sta":153},{"name":"Delphox","form":"Normal","atk":230,"def":189,"sta":181},{"name":"Froakie","form":"Normal","atk":122,"def":84,"sta":121},{"name":"Frogadier","form":"Normal","atk":168,"def":114,"sta":144},{"name":"Greninja","form":"Normal","atk":223,"def":152,"sta":176},{"name":"Bunnelby","form":"Normal","atk":68,"def":72,"sta":116},{"name":"Diggersby","form":"Normal","atk":112,"def":155,"sta":198},{"name":"Fletchling","form":"Normal","atk":95,"def":80,"sta":128},{"name":"Fletchinder","form":"Normal","atk":145,"def":110,"sta":158},{"name":"Talonflame","form":"Normal","atk":176,"def":155,"sta":186},{"name":"Scatterbug","form":"Archipelago","atk":63,"def":63,"sta":116},{"name":"Scatterbug","form":"Continental","atk":63,"def":63,"sta":116},{"name":"Scatterbug","form":"Elegant","atk":63,"def":63,"sta":116},{"name":"Scatterbug","form":"Fancy","atk":63,"def":63,"sta":116},{"name":"Scatterbug","form":"Garden","atk":63,"def":63,"sta":116},{"name":"Scatterbug","form":"High_plains","atk":63,"def":63,"sta":116},{"name":"Scatterbug","form":"Icy_snow","atk":63,"def":63,"sta":116},{"name":"Scatterbug","form":"Jungle","atk":63,"def":63,"sta":116},{"name":"Scatterbug","form":"Marine","atk":63,"def":63,"sta":116},{"name":"Scatterbug","form":"Meadow","atk":63,"def":63,"sta":116},{"name":"Scatterbug","form":"Modern","atk":63,"def":63,"sta":116},{"name":"Scatterbug","form":"Monsoon","atk":63,"def":63,"sta":116},{"name":"Scatterbug","form":"Ocean","atk":63,"def":63,"sta":116},{"name":"Scatterbug","form":"Pokeball","atk":63,"def":63,"sta":116},{"name":"Scatterbug","form":"Polar","atk":63,"def":63,"sta":116},{"name":"Scatterbug","form":"River","atk":63,"def":63,"sta":116},{"name":"Scatterbug","form":"Sandstorm","atk":63,"def":63,"sta":116},{"name":"Scatterbug","form":"Savanna","atk":63,"def":63,"sta":116},{"name":"Scatterbug","form":"Sun","atk":63,"def":63,"sta":116},{"name":"Scatterbug","form":"Tundra","atk":63,"def":63,"sta":116},{"name":"Spewpa","form":"Archipelago","atk":48,"def":89,"sta":128},{"name":"Spewpa","form":"Continental","atk":48,"def":89,"sta":128},{"name":"Spewpa","form":"Elegant","atk":48,"def":89,"sta":128},{"name":"Spewpa","form":"Fancy","atk":48,"def":89,"sta":128},{"name":"Spewpa","form":"Garden","atk":48,"def":89,"sta":128},{"name":"Spewpa","form":"High_plains","atk":48,"def":89,"sta":128},{"name":"Spewpa","form":"Icy_snow","atk":48,"def":89,"sta":128},{"name":"Spewpa","form":"Jungle","atk":48,"def":89,"sta":128},{"name":"Spewpa","form":"Marine","atk":48,"def":89,"sta":128},{"name":"Spewpa","form":"Meadow","atk":48,"def":89,"sta":128},{"name":"Spewpa","form":"Modern","atk":48,"def":89,"sta":128},{"name":"Spewpa","form":"Monsoon","atk":48,"def":89,"sta":128},{"name":"Spewpa","form":"Ocean","atk":48,"def":89,"sta":128},{"name":"Spewpa","form":"Pokeball","atk":48,"def":89,"sta":128},{"name":"Spewpa","form":"Polar","atk":48,"def":89,"sta":128},{"name":"Spewpa","form":"River","atk":48,"def":89,"sta":128},{"name":"Spewpa","form":"Sandstorm","atk":48,"def":89,"sta":128},{"name":"Spewpa","form":"Savanna","atk":48,"def":89,"sta":128},{"name":"Spewpa","form":"Sun","atk":48,"def":89,"sta":128},{"name":"Spewpa","form":"Tundra","atk":48,"def":89,"sta":128},{"name":"Vivillon","form":"Archipelago","atk":176,"def":103,"sta":190},{"name":"Vivillon","form":"Continental","atk":176,"def":103,"sta":190},{"name":"Vivillon","form":"Elegant","atk":176,"def":103,"sta":190},{"name":"Vivillon","form":"Fancy","atk":176,"def":103,"sta":190},{"name":"Vivillon","form":"Garden","atk":176,"def":103,"sta":190},{"name":"Vivillon","form":"High_plains","atk":176,"def":103,"sta":190},{"name":"Vivillon","form":"Icy_snow","atk":176,"def":103,"sta":190},{"name":"Vivillon","form":"Jungle","atk":176,"def":103,"sta":190},{"name":"Vivillon","form":"Marine","atk":176,"def":103,"sta":190},{"name":"Vivillon","form":"Meadow","atk":176,"def":103,"sta":190},{"name":"Vivillon","form":"Modern","atk":176,"def":103,"sta":190},{"name":"Vivillon","form":"Monsoon","atk":176,"def":103,"sta":190},{"name":"Vivillon","form":"Ocean","atk":176,"def":103,"sta":190},{"name":"Vivillon","form":"Pokeball","atk":176,"def":103,"sta":190},{"name":"Vivillon","form":"Polar","atk":176,"def":103,"sta":190},{"name":"Vivillon","form":"River","atk":176,"def":103,"sta":190},{"name":"Vivillon","form":"Sandstorm","atk":176,"def":103,"sta":190},{"name":"Vivillon","form":"Savanna","atk":176,"def":103,"sta":190},{"name":"Vivillon","form":"Sun","atk":176,"def":103,"sta":190},{"name":"Vivillon","form":"Tundra","atk":176,"def":103,"sta":190},{"name":"Litleo","form":"Normal","atk":139,"def":112,"sta":158},{"name":"Pyroar","form":"Female","atk":221,"def":149,"sta":200},{"name":"Pyroar","form":"Normal","atk":221,"def":149,"sta":200},{"name":"Flabebe","form":"Blue","atk":108,"def":120,"sta":127},{"name":"Flabebe","form":"Orange","atk":108,"def":120,"sta":127},{"name":"Flabebe","form":"Red","atk":108,"def":120,"sta":127},{"name":"Flabebe","form":"White","atk":108,"def":120,"sta":127},{"name":"Flabebe","form":"Yellow","atk":108,"def":120,"sta":127},{"name":"Floette","form":"Blue","atk":136,"def":151,"sta":144},{"name":"Floette","form":"Orange","atk":136,"def":151,"sta":144},{"name":"Floette","form":"Red","atk":136,"def":151,"sta":144},{"name":"Floette","form":"White","atk":136,"def":151,"sta":144},{"name":"Floette","form":"Yellow","atk":136,"def":151,"sta":144},{"name":"Florges","form":"Blue","atk":212,"def":244,"sta":186},{"name":"Florges","form":"Orange","atk":212,"def":244,"sta":186},{"name":"Florges","form":"Red","atk":212,"def":244,"sta":186},{"name":"Florges","form":"White","atk":212,"def":244,"sta":186},{"name":"Florges","form":"Yellow","atk":212,"def":244,"sta":186},{"name":"Skiddo","form":"Normal","atk":123,"def":102,"sta":165},{"name":"Gogoat","form":"Normal","atk":196,"def":146,"sta":265},{"name":"Pancham","form":"Normal","atk":145,"def":107,"sta":167},{"name":"Pangoro","form":"Normal","atk":226,"def":146,"sta":216},{"name":"Furfrou","form":"Dandy","atk":164,"def":167,"sta":181},{"name":"Furfrou","form":"Debutante","atk":164,"def":167,"sta":181},{"name":"Furfrou","form":"Diamond","atk":164,"def":167,"sta":181},{"name":"Furfrou","form":"Heart","atk":164,"def":167,"sta":181},{"name":"Furfrou","form":"Kabuki","atk":164,"def":167,"sta":181},{"name":"Furfrou","form":"La_reine","atk":164,"def":167,"sta":181},{"name":"Furfrou","form":"Matron","atk":164,"def":167,"sta":181},{"name":"Furfrou","form":"Natural","atk":164,"def":167,"sta":181},{"name":"Furfrou","form":"Pharaoh","atk":164,"def":167,"sta":181},{"name":"Furfrou","form":"Star","atk":164,"def":167,"sta":181},{"name":"Espurr","form":"Normal","atk":120,"def":114,"sta":158},{"name":"Meowstic","form":"Female","atk":166,"def":167,"sta":179},{"name":"Meowstic","form":"Normal","atk":166,"def":167,"sta":179},{"name":"Honedge","form":"Normal","atk":135,"def":139,"sta":128},{"name":"Doublade","form":"Normal","atk":188,"def":206,"sta":153},{"name":"Aegislash","form":"Blade","atk":272,"def":97,"sta":155},{"name":"Aegislash","form":"Shield","atk":97,"def":272,"sta":155},{"name":"Spritzee","form":"Normal","atk":110,"def":113,"sta":186},{"name":"Aromatisse","form":"Normal","atk":173,"def":150,"sta":226},{"name":"Swirlix","form":"Normal","atk":109,"def":119,"sta":158},{"name":"Slurpuff","form":"Normal","atk":168,"def":163,"sta":193},{"name":"Inkay","form":"Normal","atk":98,"def":95,"sta":142},{"name":"Malamar","form":"Normal","atk":177,"def":165,"sta":200},{"name":"Binacle","form":"Normal","atk":96,"def":120,"sta":123},{"name":"Barbaracle","form":"Normal","atk":194,"def":205,"sta":176},{"name":"Skrelp","form":"Normal","atk":109,"def":109,"sta":137},{"name":"Dragalge","form":"Normal","atk":177,"def":207,"sta":163},{"name":"Clauncher","form":"Normal","atk":108,"def":117,"sta":137},{"name":"Clawitzer","form":"Normal","atk":221,"def":171,"sta":174},{"name":"Helioptile","form":"Normal","atk":115,"def":78,"sta":127},{"name":"Heliolisk","form":"Normal","atk":219,"def":168,"sta":158},{"name":"Tyrunt","form":"Normal","atk":158,"def":123,"sta":151},{"name":"Tyrantrum","form":"Normal","atk":227,"def":191,"sta":193},{"name":"Amaura","form":"Normal","atk":124,"def":109,"sta":184},{"name":"Aurorus","form":"Normal","atk":186,"def":163,"sta":265},{"name":"Sylveon","form":"Normal","atk":203,"def":205,"sta":216},{"name":"Hawlucha","form":"Normal","atk":195,"def":153,"sta":186},{"name":"Dedenne","form":"Normal","atk":164,"def":134,"sta":167},{"name":"Carbink","form":"Normal","atk":95,"def":285,"sta":137},{"name":"Goomy","form":"Normal","atk":101,"def":112,"sta":128},{"name":"Sliggoo","form":"Normal","atk":159,"def":176,"sta":169},{"name":"Goodra","form":"Normal","atk":220,"def":242,"sta":207},{"name":"Klefki","form":"Normal","atk":160,"def":179,"sta":149},{"name":"Phantump","form":"Normal","atk":125,"def":103,"sta":125},{"name":"Trevenant","form":"Normal","atk":201,"def":154,"sta":198},{"name":"Pumpkaboo","form":"Average","atk":121,"def":123,"sta":135},{"name":"Pumpkaboo","form":"Large","atk":120,"def":122,"sta":144},{"name":"Pumpkaboo","form":"Small","atk":122,"def":124,"sta":127},{"name":"Pumpkaboo","form":"Super","atk":118,"def":120,"sta":153},{"name":"Gourgeist","form":"Average","atk":175,"def":213,"sta":163},{"name":"Gourgeist","form":"Large","atk":179,"def":206,"sta":181},{"name":"Gourgeist","form":"Small","atk":171,"def":219,"sta":146},{"name":"Gourgeist","form":"Super","atk":182,"def":200,"sta":198},{"name":"Bergmite","form":"Normal","atk":117,"def":120,"sta":146},{"name":"Avalugg","form":"Hisuian","atk":214,"def":238,"sta":216},{"name":"Avalugg","form":"Normal","atk":196,"def":240,"sta":216},{"name":"Noibat","form":"Normal","atk":83,"def":73,"sta":120},{"name":"Noivern","form":"Normal","atk":205,"def":175,"sta":198},{"name":"Xerneas","form":"Normal","atk":250,"def":185,"sta":246},{"name":"Yveltal","form":"Normal","atk":250,"def":185,"sta":246},{"name":"Zygarde","form":"Complete","atk":184,"def":207,"sta":389},{"name":"Zygarde","form":"Complete_fifty_percent","atk":203,"def":232,"sta":239},{"name":"Zygarde","form":"Complete_ten_percent","atk":205,"def":173,"sta":144},{"name":"Zygarde","form":"Fifty_percent","atk":203,"def":232,"sta":239},{"name":"Zygarde","form":"Ten_percent","atk":205,"def":173,"sta":144},{"name":"Diancie","form":"Normal","atk":190,"def":285,"sta":137},{"name":"Hoopa","form":"Confined","atk":261,"def":187,"sta":173},{"name":"Hoopa","form":"Unbound","atk":311,"def":191,"sta":173},{"name":"Volcanion","form":"Normal","atk":252,"def":216,"sta":190},{"name":"Rowlet","form":"Normal","atk":102,"def":99,"sta":169},{"name":"Dartrix","form":"Normal","atk":142,"def":139,"sta":186},{"name":"Decidueye","form":"Hisuian","atk":213,"def":174,"sta":204},{"name":"Decidueye","form":"Normal","atk":210,"def":179,"sta":186},{"name":"Litten","form":"Normal","atk":128,"def":79,"sta":128},{"name":"Torracat","form":"Normal","atk":174,"def":103,"sta":163},{"name":"Incineroar","form":"Normal","atk":214,"def":175,"sta":216},{"name":"Popplio","form":"Normal","atk":120,"def":103,"sta":137},{"name":"Brionne","form":"Normal","atk":168,"def":145,"sta":155},{"name":"Primarina","form":"Normal","atk":232,"def":195,"sta":190},{"name":"Pikipek","form":"Normal","atk":136,"def":59,"sta":111},{"name":"Trumbeak","form":"Normal","atk":159,"def":100,"sta":146},{"name":"Toucannon","form":"Normal","atk":222,"def":146,"sta":190},{"name":"Yungoos","form":"Normal","atk":122,"def":56,"sta":134},{"name":"Gumshoos","form":"Normal","atk":194,"def":113,"sta":204},{"name":"Grubbin","form":"Normal","atk":115,"def":85,"sta":132},{"name":"Charjabug","form":"Normal","atk":145,"def":161,"sta":149},{"name":"Vikavolt","form":"Normal","atk":254,"def":158,"sta":184},{"name":"Crabrawler","form":"Normal","atk":150,"def":104,"sta":132},{"name":"Crabominable","form":"Normal","atk":231,"def":138,"sta":219},{"name":"Oricorio","form":"Baile","atk":196,"def":145,"sta":181},{"name":"Oricorio","form":"Pau","atk":196,"def":145,"sta":181},{"name":"Oricorio","form":"Pompom","atk":196,"def":145,"sta":181},{"name":"Oricorio","form":"Sensu","atk":196,"def":145,"sta":181},{"name":"Cutiefly","form":"Normal","atk":110,"def":81,"sta":120},{"name":"Ribombee","form":"Normal","atk":198,"def":146,"sta":155},{"name":"Rockruff","form":"Dusk","atk":117,"def":78,"sta":128},{"name":"Rockruff","form":"Normal","atk":117,"def":78,"sta":128},{"name":"Lycanroc","form":"Dusk","atk":234,"def":139,"sta":181},{"name":"Lycanroc","form":"Midday","atk":231,"def":140,"sta":181},{"name":"Lycanroc","form":"Midnight","atk":218,"def":152,"sta":198},{"name":"Wishiwashi","form":"School","atk":255,"def":242,"sta":128},{"name":"Wishiwashi","form":"Solo","atk":46,"def":43,"sta":128},{"name":"Mareanie","form":"Normal","atk":98,"def":110,"sta":137},{"name":"Toxapex","form":"Normal","atk":114,"def":273,"sta":137},{"name":"Mudbray","form":"Normal","atk":175,"def":121,"sta":172},{"name":"Mudsdale","form":"Normal","atk":214,"def":174,"sta":225},{"name":"Dewpider","form":"Normal","atk":72,"def":117,"sta":116},{"name":"Araquanid","form":"Normal","atk":126,"def":219,"sta":169},{"name":"Fomantis","form":"Normal","atk":100,"def":64,"sta":120},{"name":"Lurantis","form":"Normal","atk":192,"def":169,"sta":172},{"name":"Morelull","form":"Normal","atk":108,"def":119,"sta":120},{"name":"Shiinotic","form":"Normal","atk":154,"def":168,"sta":155},{"name":"Salandit","form":"Normal","atk":136,"def":80,"sta":134},{"name":"Salazzle","form":"Normal","atk":228,"def":130,"sta":169},{"name":"Stufful","form":"Normal","atk":136,"def":95,"sta":172},{"name":"Bewear","form":"Normal","atk":226,"def":141,"sta":260},{"name":"Bewear","form":"Wildarea_2025","atk":226,"def":141,"sta":260},{"name":"Bounsweet","form":"Normal","atk":55,"def":69,"sta":123},{"name":"Steenee","form":"Normal","atk":78,"def":94,"sta":141},{"name":"Tsareena","form":"Normal","atk":222,"def":195,"sta":176},{"name":"Comfey","form":"Normal","atk":165,"def":215,"sta":139},{"name":"Oranguru","form":"Normal","atk":168,"def":192,"sta":207},{"name":"Passimian","form":"Normal","atk":222,"def":160,"sta":225},{"name":"Wimpod","form":"Normal","atk":67,"def":74,"sta":93},{"name":"Golisopod","form":"Normal","atk":218,"def":226,"sta":181},{"name":"Sandygast","form":"Normal","atk":120,"def":118,"sta":146},{"name":"Palossand","form":"Normal","atk":178,"def":178,"sta":198},{"name":"Pyukumuku","form":"Normal","atk":97,"def":224,"sta":146},{"name":"Type: Null","form":"Normal","atk":184,"def":184,"sta":216},{"name":"Silvally","form":"Bug","atk":198,"def":198,"sta":216},{"name":"Silvally","form":"Dark","atk":198,"def":198,"sta":216},{"name":"Silvally","form":"Dragon","atk":198,"def":198,"sta":216},{"name":"Silvally","form":"Electric","atk":198,"def":198,"sta":216},{"name":"Silvally","form":"Fairy","atk":198,"def":198,"sta":216},{"name":"Silvally","form":"Fighting","atk":198,"def":198,"sta":216},{"name":"Silvally","form":"Fire","atk":198,"def":198,"sta":216},{"name":"Silvally","form":"Flying","atk":198,"def":198,"sta":216},{"name":"Silvally","form":"Ghost","atk":198,"def":198,"sta":216},{"name":"Silvally","form":"Grass","atk":198,"def":198,"sta":216},{"name":"Silvally","form":"Ground","atk":198,"def":198,"sta":216},{"name":"Silvally","form":"Ice","atk":198,"def":198,"sta":216},{"name":"Silvally","form":"Normal","atk":198,"def":198,"sta":216},{"name":"Silvally","form":"Poison","atk":198,"def":198,"sta":216},{"name":"Silvally","form":"Psychic","atk":198,"def":198,"sta":216},{"name":"Silvally","form":"Rock","atk":198,"def":198,"sta":216},{"name":"Silvally","form":"Steel","atk":198,"def":198,"sta":216},{"name":"Silvally","form":"Water","atk":198,"def":198,"sta":216},{"name":"Minior","form":"Blue","atk":218,"def":131,"sta":155},{"name":"Minior","form":"Green","atk":218,"def":131,"sta":155},{"name":"Minior","form":"Indigo","atk":218,"def":131,"sta":155},{"name":"Minior","form":"Orange","atk":218,"def":131,"sta":155},{"name":"Minior","form":"Red","atk":218,"def":131,"sta":155},{"name":"Minior","form":"Violet","atk":218,"def":131,"sta":155},{"name":"Minior","form":"Yellow","atk":218,"def":131,"sta":155},{"name":"Komala","form":"Normal","atk":216,"def":165,"sta":163},{"name":"Turtonator","form":"Normal","atk":165,"def":215,"sta":155},{"name":"Togedemaru","form":"Normal","atk":190,"def":145,"sta":163},{"name":"Mimikyu","form":"Busted","atk":177,"def":199,"sta":146},{"name":"Mimikyu","form":"Disguised","atk":177,"def":199,"sta":146},{"name":"Bruxish","form":"Normal","atk":208,"def":145,"sta":169},{"name":"Drampa","form":"Normal","atk":231,"def":164,"sta":186},{"name":"Dhelmise","form":"Normal","atk":233,"def":179,"sta":172},{"name":"Jangmo-o","form":"Normal","atk":102,"def":108,"sta":128},{"name":"Hakamo-o","form":"Normal","atk":145,"def":162,"sta":146},{"name":"Kommo-o","form":"Normal","atk":222,"def":240,"sta":181},{"name":"Tapu Koko","form":"Normal","atk":250,"def":181,"sta":172},{"name":"Tapu Lele","form":"Normal","atk":259,"def":208,"sta":172},{"name":"Tapu Bulu","form":"Normal","atk":249,"def":215,"sta":172},{"name":"Tapu Fini","form":"Normal","atk":189,"def":254,"sta":172},{"name":"Cosmog","form":"Normal","atk":54,"def":57,"sta":125},{"name":"Cosmoem","form":"Normal","atk":54,"def":242,"sta":125},{"name":"Solgaleo","form":"Normal","atk":255,"def":191,"sta":264},{"name":"Lunala","form":"Normal","atk":255,"def":191,"sta":264},{"name":"Nihilego","form":"Normal","atk":249,"def":210,"sta":240},{"name":"Buzzwole","form":"Normal","atk":236,"def":196,"sta":216},{"name":"Pheromosa","form":"Normal","atk":316,"def":85,"sta":174},{"name":"Xurkitree","form":"Normal","atk":330,"def":144,"sta":195},{"name":"Celesteela","form":"Normal","atk":207,"def":199,"sta":219},{"name":"Kartana","form":"Normal","atk":323,"def":182,"sta":139},{"name":"Guzzlord","form":"Normal","atk":188,"def":99,"sta":440},{"name":"Necrozma","form":"Dawn_wings","atk":277,"def":220,"sta":200},{"name":"Necrozma","form":"Dusk_mane","atk":277,"def":220,"sta":200},{"name":"Necrozma","form":"Normal","atk":251,"def":195,"sta":219},{"name":"Necrozma","form":"Ultra","atk":337,"def":196,"sta":200},{"name":"Magearna","form":"Normal","atk":246,"def":225,"sta":190},{"name":"Magearna","form":"Original_color","atk":246,"def":225,"sta":190},{"name":"Marshadow","form":"Normal","atk":265,"def":190,"sta":207},{"name":"Poipole","form":"Normal","atk":145,"def":133,"sta":167},{"name":"Naganadel","form":"Normal","atk":263,"def":159,"sta":177},{"name":"Stakataka","form":"Normal","atk":213,"def":298,"sta":156},{"name":"Blacephalon","form":"Normal","atk":315,"def":148,"sta":142},{"name":"Zeraora","form":"Normal","atk":252,"def":177,"sta":204},{"name":"Meltan","form":"Normal","atk":118,"def":99,"sta":130},{"name":"Melmetal","form":"Normal","atk":226,"def":190,"sta":264},{"name":"Grookey","form":"Normal","atk":122,"def":91,"sta":137},{"name":"Thwackey","form":"Normal","atk":165,"def":134,"sta":172},{"name":"Rillaboom","form":"Normal","atk":239,"def":168,"sta":225},{"name":"Scorbunny","form":"Normal","atk":132,"def":79,"sta":137},{"name":"Raboot","form":"Normal","atk":170,"def":125,"sta":163},{"name":"Cinderace","form":"Normal","atk":238,"def":163,"sta":190},{"name":"Sobble","form":"Normal","atk":132,"def":79,"sta":137},{"name":"Drizzile","form":"Normal","atk":186,"def":113,"sta":163},{"name":"Inteleon","form":"Normal","atk":262,"def":142,"sta":172},{"name":"Skwovet","form":"Normal","atk":95,"def":86,"sta":172},{"name":"Greedent","form":"Normal","atk":160,"def":156,"sta":260},{"name":"Rookidee","form":"Normal","atk":88,"def":67,"sta":116},{"name":"Corvisquire","form":"Normal","atk":129,"def":110,"sta":169},{"name":"Corviknight","form":"Normal","atk":163,"def":192,"sta":221},{"name":"Blipbug","form":"Normal","atk":46,"def":67,"sta":93},{"name":"Dottler","form":"Normal","atk":87,"def":157,"sta":137},{"name":"Orbeetle","form":"Normal","atk":156,"def":240,"sta":155},{"name":"Nickit","form":"Normal","atk":85,"def":82,"sta":120},{"name":"Thievul","form":"Normal","atk":172,"def":164,"sta":172},{"name":"Gossifleur","form":"Normal","atk":70,"def":104,"sta":120},{"name":"Eldegoss","form":"Normal","atk":148,"def":211,"sta":155},{"name":"Wooloo","form":"Normal","atk":76,"def":97,"sta":123},{"name":"Dubwool","form":"Normal","atk":159,"def":198,"sta":176},{"name":"Chewtle","form":"Normal","atk":114,"def":85,"sta":137},{"name":"Drednaw","form":"Normal","atk":213,"def":164,"sta":207},{"name":"Yamper","form":"Normal","atk":80,"def":90,"sta":153},{"name":"Boltund","form":"Normal","atk":197,"def":131,"sta":170},{"name":"Rolycoly","form":"Normal","atk":73,"def":91,"sta":102},{"name":"Carkol","form":"Normal","atk":114,"def":157,"sta":190},{"name":"Coalossal","form":"Normal","atk":146,"def":198,"sta":242},{"name":"Applin","form":"Normal","atk":71,"def":116,"sta":120},{"name":"Flapple","form":"Normal","atk":214,"def":144,"sta":172},{"name":"Appletun","form":"Normal","atk":178,"def":146,"sta":242},{"name":"Silicobra","form":"Normal","atk":103,"def":123,"sta":141},{"name":"Sandaconda","form":"Normal","atk":202,"def":207,"sta":176},{"name":"Cramorant","form":"Normal","atk":173,"def":163,"sta":172},{"name":"Arrokuda","form":"Normal","atk":118,"def":72,"sta":121},{"name":"Barraskewda","form":"Normal","atk":258,"def":127,"sta":156},{"name":"Toxel","form":"Normal","atk":97,"def":65,"sta":120},{"name":"Toxtricity","form":"Amped","atk":224,"def":140,"sta":181},{"name":"Toxtricity","form":"Low_key","atk":224,"def":140,"sta":181},{"name":"Sizzlipede","form":"Normal","atk":118,"def":90,"sta":137},{"name":"Centiskorch","form":"Normal","atk":220,"def":158,"sta":225},{"name":"Clobbopus","form":"Normal","atk":121,"def":103,"sta":137},{"name":"Grapploct","form":"Normal","atk":209,"def":162,"sta":190},{"name":"Sinistea","form":"Antique","atk":134,"def":96,"sta":120},{"name":"Sinistea","form":"Phony","atk":134,"def":96,"sta":120},{"name":"Polteageist","form":"Antique","atk":248,"def":189,"sta":155},{"name":"Polteageist","form":"Phony","atk":248,"def":189,"sta":155},{"name":"Hatenna","form":"Normal","atk":98,"def":93,"sta":123},{"name":"Hattrem","form":"Normal","atk":153,"def":133,"sta":149},{"name":"Hatterene","form":"Normal","atk":237,"def":182,"sta":149},{"name":"Impidimp","form":"Normal","atk":103,"def":69,"sta":128},{"name":"Morgrem","form":"Normal","atk":145,"def":102,"sta":163},{"name":"Grimmsnarl","form":"Normal","atk":227,"def":139,"sta":216},{"name":"Obstagoon","form":"Galarian","atk":180,"def":194,"sta":212},{"name":"Perrserker","form":"Galarian","atk":195,"def":162,"sta":172},{"name":"Cursola","form":"Normal","atk":253,"def":182,"sta":155},{"name":"Sirfetch’d","form":"Galarian","atk":248,"def":176,"sta":158},{"name":"Mr. Rime","form":"Galarian","atk":212,"def":179,"sta":190},{"name":"Runerigus","form":"Galarian","atk":163,"def":237,"sta":151},{"name":"Milcery","form":"Normal","atk":90,"def":97,"sta":128},{"name":"Alcremie","form":"Normal","atk":203,"def":203,"sta":163},{"name":"Falinks","form":"Gofest_2025_train_conductor","atk":193,"def":170,"sta":163},{"name":"Falinks","form":"Normal","atk":193,"def":170,"sta":163},{"name":"Pincurchin","form":"Normal","atk":176,"def":161,"sta":134},{"name":"Snom","form":"Normal","atk":76,"def":59,"sta":102},{"name":"Frosmoth","form":"Normal","atk":230,"def":155,"sta":172},{"name":"Stonjourner","form":"Normal","atk":222,"def":182,"sta":225},{"name":"Eiscue","form":"Ice","atk":148,"def":195,"sta":181},{"name":"Eiscue","form":"Noice","atk":173,"def":139,"sta":181},{"name":"Indeedee","form":"Female","atk":184,"def":184,"sta":172},{"name":"Indeedee","form":"Male","atk":208,"def":166,"sta":155},{"name":"Morpeko","form":"Full_belly","atk":192,"def":121,"sta":151},{"name":"Morpeko","form":"Hangry","atk":192,"def":121,"sta":151},{"name":"Cufant","form":"Normal","atk":140,"def":91,"sta":176},{"name":"Copperajah","form":"Normal","atk":226,"def":126,"sta":263},{"name":"Dracozolt","form":"Normal","atk":195,"def":165,"sta":207},{"name":"Arctozolt","form":"Normal","atk":190,"def":166,"sta":207},{"name":"Dracovish","form":"Normal","atk":175,"def":185,"sta":207},{"name":"Arctovish","form":"Normal","atk":171,"def":185,"sta":207},{"name":"Duraludon","form":"Normal","atk":239,"def":185,"sta":172},{"name":"Dreepy","form":"Normal","atk":117,"def":61,"sta":99},{"name":"Drakloak","form":"Normal","atk":163,"def":105,"sta":169},{"name":"Dragapult","form":"Normal","atk":266,"def":170,"sta":204},{"name":"Zacian","form":"Crowned_sword","atk":332,"def":240,"sta":192},{"name":"Zacian","form":"Hero","atk":254,"def":236,"sta":192},{"name":"Zamazenta","form":"Crowned_shield","atk":250,"def":292,"sta":192},{"name":"Zamazenta","form":"Hero","atk":254,"def":236,"sta":192},{"name":"Eternatus","form":"Eternamax","atk":251,"def":505,"sta":452},{"name":"Eternatus","form":"Normal","atk":278,"def":192,"sta":268},{"name":"Kubfu","form":"Normal","atk":170,"def":112,"sta":155},{"name":"Urshifu","form":"Rapid_strike","atk":254,"def":177,"sta":225},{"name":"Urshifu","form":"Single_strike","atk":254,"def":177,"sta":225},{"name":"Zarude","form":"Normal","atk":242,"def":215,"sta":233},{"name":"Regieleki","form":"Normal","atk":250,"def":125,"sta":190},{"name":"Regidrago","form":"Normal","atk":202,"def":101,"sta":400},{"name":"Glastrier","form":"Normal","atk":246,"def":223,"sta":225},{"name":"Spectrier","form":"Normal","atk":273,"def":146,"sta":205},{"name":"Calyrex","form":"Ice_rider","atk":268,"def":246,"sta":205},{"name":"Calyrex","form":"Normal","atk":162,"def":162,"sta":225},{"name":"Calyrex","form":"Shadow_rider","atk":324,"def":194,"sta":205},{"name":"Wyrdeer","form":"Normal","atk":206,"def":145,"sta":230},{"name":"Kleavor","form":"Normal","atk":253,"def":174,"sta":172},{"name":"Ursaluna","form":"Normal","atk":243,"def":181,"sta":277},{"name":"Sneasler","form":"Normal","atk":259,"def":158,"sta":190},{"name":"Overqwil","form":"Normal","atk":222,"def":171,"sta":198},{"name":"Enamorus","form":"Incarnate","atk":281,"def":162,"sta":179},{"name":"Enamorus","form":"Therian","atk":250,"def":201,"sta":179},{"name":"Sprigatito","form":"Normal","atk":116,"def":99,"sta":120},{"name":"Floragato","form":"Normal","atk":157,"def":128,"sta":156},{"name":"Meowscarada","form":"Normal","atk":233,"def":153,"sta":183},{"name":"Fuecoco","form":"Normal","atk":112,"def":96,"sta":167},{"name":"Crocalor","form":"Normal","atk":162,"def":134,"sta":191},{"name":"Skeledirge","form":"Normal","atk":207,"def":178,"sta":232},{"name":"Quaxly","form":"Normal","atk":120,"def":86,"sta":146},{"name":"Quaxwell","form":"Normal","atk":162,"def":123,"sta":172},{"name":"Quaquaval","form":"Normal","atk":236,"def":159,"sta":198},{"name":"Lechonk","form":"Normal","atk":81,"def":79,"sta":144},{"name":"Oinkologne","form":"Female","atk":169,"def":162,"sta":251},{"name":"Oinkologne","form":"Normal","atk":186,"def":153,"sta":242},{"name":"Tarountula","form":"Normal","atk":70,"def":77,"sta":111},{"name":"Spidops","form":"Normal","atk":139,"def":166,"sta":155},{"name":"Nymble","form":"Normal","atk":81,"def":65,"sta":107},{"name":"Lokix","form":"Normal","atk":199,"def":144,"sta":174},{"name":"Pawmi","form":"Normal","atk":95,"def":45,"sta":128},{"name":"Pawmo","form":"Normal","atk":147,"def":82,"sta":155},{"name":"Pawmot","form":"Normal","atk":232,"def":141,"sta":172},{"name":"Tandemaus","form":"Normal","atk":98,"def":90,"sta":137},{"name":"Maushold","form":"Family_of_four","atk":159,"def":157,"sta":179},{"name":"Maushold","form":"Family_of_three","atk":159,"def":157,"sta":179},{"name":"Fidough","form":"Normal","atk":102,"def":126,"sta":114},{"name":"Dachsbun","form":"Normal","atk":159,"def":212,"sta":149},{"name":"Smoliv","form":"Normal","atk":100,"def":89,"sta":121},{"name":"Dolliv","form":"Normal","atk":137,"def":131,"sta":141},{"name":"Arboliva","form":"Normal","atk":219,"def":189,"sta":186},{"name":"Squawkabilly","form":"Blue","atk":185,"def":105,"sta":193},{"name":"Squawkabilly","form":"Green","atk":185,"def":105,"sta":193},{"name":"Squawkabilly","form":"White","atk":185,"def":105,"sta":193},{"name":"Squawkabilly","form":"Yellow","atk":185,"def":105,"sta":193},{"name":"Nacli","form":"Normal","atk":95,"def":108,"sta":146},{"name":"Naclstack","form":"Normal","atk":105,"def":160,"sta":155},{"name":"Garganacl","form":"Normal","atk":171,"def":212,"sta":225},{"name":"Charcadet","form":"Normal","atk":92,"def":74,"sta":120},{"name":"Armarouge","form":"Normal","atk":234,"def":185,"sta":198},{"name":"Ceruledge","form":"Normal","atk":239,"def":189,"sta":181},{"name":"Tadbulb","form":"Normal","atk":104,"def":73,"sta":156},{"name":"Bellibolt","form":"Normal","atk":184,"def":165,"sta":240},{"name":"Wattrel","form":"Normal","atk":105,"def":75,"sta":120},{"name":"Kilowattrel","form":"Normal","atk":221,"def":132,"sta":172},{"name":"Maschiff","form":"Normal","atk":140,"def":108,"sta":155},{"name":"Mabosstiff","form":"Normal","atk":230,"def":168,"sta":190},{"name":"Shroodle","form":"Normal","atk":124,"def":70,"sta":120},{"name":"Grafaiai","form":"Normal","atk":199,"def":149,"sta":160},{"name":"Bramblin","form":"Normal","atk":121,"def":64,"sta":120},{"name":"Brambleghast","form":"Normal","atk":228,"def":144,"sta":146},{"name":"Toedscool","form":"Normal","atk":97,"def":149,"sta":120},{"name":"Toedscruel","form":"Normal","atk":166,"def":209,"sta":190},{"name":"Klawf","form":"Normal","atk":184,"def":185,"sta":172},{"name":"Capsakid","form":"Normal","atk":118,"def":76,"sta":137},{"name":"Scovillain","form":"Normal","atk":216,"def":130,"sta":163},{"name":"Rellor","form":"Normal","atk":86,"def":108,"sta":121},{"name":"Rabsca","form":"Normal","atk":201,"def":178,"sta":181},{"name":"Flittle","form":"Normal","atk":105,"def":60,"sta":102},{"name":"Espathra","form":"Normal","atk":204,"def":127,"sta":216},{"name":"Tinkatink","form":"Normal","atk":85,"def":110,"sta":137},{"name":"Tinkatuff","form":"Normal","atk":109,"def":145,"sta":163},{"name":"Tinkaton","form":"Normal","atk":155,"def":196,"sta":198},{"name":"Wiglett","form":"Normal","atk":109,"def":52,"sta":67},{"name":"Wugtrio","form":"Normal","atk":205,"def":136,"sta":111},{"name":"Bombirdier","form":"Normal","atk":198,"def":172,"sta":172},{"name":"Finizen","form":"Normal","atk":90,"def":80,"sta":172},{"name":"Palafin","form":"Hero","atk":322,"def":196,"sta":225},{"name":"Palafin","form":"Zero","atk":143,"def":144,"sta":225},{"name":"Varoom","form":"Normal","atk":123,"def":107,"sta":128},{"name":"Revavroom","form":"Normal","atk":229,"def":168,"sta":190},{"name":"Cyclizar","form":"Normal","atk":205,"def":142,"sta":172},{"name":"Orthworm","form":"Normal","atk":161,"def":219,"sta":172},{"name":"Glimmet","form":"Normal","atk":187,"def":104,"sta":134},{"name":"Glimmora","form":"Normal","atk":246,"def":177,"sta":195},{"name":"Greavard","form":"Normal","atk":105,"def":106,"sta":137},{"name":"Houndstone","form":"Normal","atk":186,"def":195,"sta":176},{"name":"Flamigo","form":"Normal","atk":227,"def":145,"sta":193},{"name":"Cetoddle","form":"Normal","atk":119,"def":80,"sta":239},{"name":"Cetitan","form":"Normal","atk":208,"def":123,"sta":347},{"name":"Veluza","form":"Normal","atk":196,"def":139,"sta":207},{"name":"Dondozo","form":"Normal","atk":176,"def":178,"sta":312},{"name":"Tatsugiri","form":"Curly","atk":226,"def":166,"sta":169},{"name":"Tatsugiri","form":"Droopy","atk":226,"def":166,"sta":169},{"name":"Tatsugiri","form":"Stretchy","atk":226,"def":166,"sta":169},{"name":"Annihilape","form":"Normal","atk":220,"def":178,"sta":242},{"name":"Clodsire","form":"Normal","atk":127,"def":151,"sta":277},{"name":"Farigiraf","form":"Normal","atk":209,"def":136,"sta":260},{"name":"Dudunsparce","form":"Three","atk":188,"def":150,"sta":268},{"name":"Dudunsparce","form":"Two","atk":188,"def":150,"sta":268},{"name":"Kingambit","form":"Normal","atk":238,"def":203,"sta":225},{"name":"Great Tusk","form":"Normal","atk":249,"def":209,"sta":251},{"name":"Scream Tail","form":"Normal","atk":139,"def":234,"sta":251},{"name":"Brute Bonnet","form":"Normal","atk":232,"def":190,"sta":244},{"name":"Flutter Mane","form":"Normal","atk":280,"def":235,"sta":146},{"name":"Slither Wing","form":"Normal","atk":261,"def":193,"sta":198},{"name":"Sandy Shocks","form":"Normal","atk":244,"def":195,"sta":198},{"name":"Iron Treads","form":"Normal","atk":227,"def":216,"sta":207},{"name":"Iron Bundle","form":"Normal","atk":266,"def":211,"sta":148},{"name":"Iron Hands","form":"Normal","atk":245,"def":177,"sta":319},{"name":"Iron Jugulis","form":"Normal","atk":249,"def":179,"sta":214},{"name":"Iron Moth","form":"Normal","atk":281,"def":196,"sta":190},{"name":"Iron Thorns","form":"Normal","atk":250,"def":200,"sta":225},{"name":"Frigibax","form":"Normal","atk":134,"def":86,"sta":163},{"name":"Arctibax","form":"Normal","atk":173,"def":128,"sta":207},{"name":"Baxcalibur","form":"Normal","atk":254,"def":168,"sta":229},{"name":"Gimmighoul","form":"Coin_a1","atk":140,"def":76,"sta":128},{"name":"Gimmighoul","form":"Normal","atk":140,"def":76,"sta":128},{"name":"Gholdengo","form":"Normal","atk":252,"def":190,"sta":202},{"name":"Wo-Chien","form":"Normal","atk":186,"def":242,"sta":198},{"name":"Chien-Pao","form":"Normal","atk":261,"def":167,"sta":190},{"name":"Ting-Lu","form":"Normal","atk":194,"def":203,"sta":321},{"name":"Chi-Yu","form":"Normal","atk":269,"def":221,"sta":146},{"name":"Roaring Moon","form":"Normal","atk":280,"def":196,"sta":233},{"name":"Iron Valiant","form":"Normal","atk":279,"def":171,"sta":179},{"name":"Koraidon","form":"Apex","atk":263,"def":223,"sta":205},{"name":"Miraidon","form":"Ultimate","atk":263,"def":223,"sta":205},{"name":"Walking Wake","form":"Normal","atk":233,"def":171,"sta":203},{"name":"Iron Leaves","form":"Normal","atk":236,"def":194,"sta":189},{"name":"Dipplin","form":"Normal","atk":173,"def":184,"sta":190},{"name":"Poltchageist","form":"Artisan","atk":134,"def":96,"sta":120},{"name":"Poltchageist","form":"Counterfeit","atk":134,"def":96,"sta":120},{"name":"Sinistcha","form":"Masterpiece","atk":225,"def":191,"sta":174},{"name":"Sinistcha","form":"Unremarkable","atk":225,"def":191,"sta":174},{"name":"Okidogi","form":"Normal","atk":220,"def":191,"sta":186},{"name":"Munkidori","form":"Normal","atk":238,"def":157,"sta":186},{"name":"Fezandipiti","form":"Normal","atk":169,"def":208,"sta":186},{"name":"Ogerpon","form":"Normal","atk":219,"def":178,"sta":173},{"name":"Archaludon","form":"Normal","atk":250,"def":215,"sta":207},{"name":"Hydrapple","form":"Normal","atk":216,"def":186,"sta":235},{"name":"Gouging Fire","form":"Normal","atk":205,"def":208,"sta":213},{"name":"Raging Bolt","form":"Normal","atk":235,"def":165,"sta":245},{"name":"Iron Boulder","form":"Normal","atk":227,"def":195,"sta":189},{"name":"Iron Crown","form":"Normal","atk":221,"def":200,"sta":189},{"name":"Terapagos","form":"Normal","atk":126,"def":165,"sta":207},{"name":"Pecharunt","form":"Normal","atk":164,"def":248,"sta":186}]} diff --git a/data/shiny-pokemon.json b/data/shiny-pokemon.json new file mode 100644 index 0000000..93eb614 --- /dev/null +++ b/data/shiny-pokemon.json @@ -0,0 +1 @@ +{"generated":"2026-08-02T03:09:47.850Z","source":"https://pogoapi.net/api/v1/shiny_pokemon.json","names":["Abomasnow","Abra","Absol","Accelgor","Aerodactyl","Aggron","Aipom","Alakazam","Alomomola","Altaria","Amaura","Ambipom","Amoonguss","Ampharos","Annihilape","Anorith","Araquanid","Arbok","Arboliva","Arcanine","Archen","Archeops","Arctibax","Ariados","Armaldo","Armarouge","Aromatisse","Aron","Articuno","Audino","Aurorus","Avalugg","Axew","Azelf","Azumarill","Azurill","Bagon","Baltoy","Banette","Barbaracle","Barboach","Basculin","Bastiodon","Baxcalibur","Bayleef","Beartic","Beautifly","Beedrill","Beheeyem","Beldum","Bellibolt","Bellossom","Bellsprout","Bergmite","Bewear","Bibarel","Bidoof","Binacle","Bisharp","Blacephalon","Blastoise","Blaziken","Blissey","Blitzle","Boldore","Bombirdier","Bonsly","Bouffalant","Bounsweet","Braixen","Braviary","Breloom","Brionne","Bronzong","Bronzor","Bruxish","Budew","Buizel","Bulbasaur","Buneary","Bunnelby","Burmy","Butterfree","Buzzwole","Cacnea","Cacturne","Camerupt","Carbink","Carnivine","Carracosta","Carvanha","Cascoon","Castform","Caterpie","Celebi","Celesteela","Ceruledge","Cetitan","Cetoddle","Chandelure","Chansey","Charcadet","Charizard","Charjabug","Charmander","Charmeleon","Chatot","Cherrim","Cherubi","Chesnaught","Chespin","Chikorita","Chimchar","Chimecho","Chinchou","Chingling","Cinccino","Clamperl","Clauncher","Clawitzer","Claydol","Clefable","Clefairy","Cleffa","Clodsire","Cloyster","Cobalion","Cofagrigus","Combee","Combusken","Conkeldurr","Corphish","Corsola","Corviknight","Corvisquire","Cottonee","Crabominable","Crabrawler","Cradily","Cranidos","Crawdaunt","Cresselia","Croagunk","Crobat","Crocalor","Croconaw","Crustle","Cryogonal","Cubchoo","Cubone","Cursola","Cutiefly","Cyndaquil","Dachsbun","Darkrai","Darmanitan","Dartrix","Darumaka","Decidueye","Dedenne","Deerling","Deino","Delcatty","Delibird","Delphox","Deoxys","Dewgong","Dewott","Dewpider","Dhelmise","Dialga","Diggersby","Diglett","Ditto","Dodrio","Doduo","Dolliv","Dondozo","Donphan","Dragalge","Dragonair","Dragonite","Drampa","Drapion","Dratini","Drifblim","Drifloon","Drilbur","Drowzee","Druddigon","Dubwool","Ducklett","Dudunsparce","Dugtrio","Dunsparce","Duosion","Duraludon","Durant","Dusclops","Dusknoir","Duskull","Dustox","Dwebble","Eelektrik","Eelektross","Eevee","Ekans","Electabuzz","Electivire","Electrike","Electrode","Elekid","Elgyem","Emboar","Emolga","Empoleon","Entei","Escavalier","Espeon","Espurr","Excadrill","Exeggcute","Exeggutor","Exploud","Falinks","Farfetch’d","Fearow","Feebas","Fennekin","Feraligatr","Ferroseed","Ferrothorn","Fidough","Finneon","Flaaffy","Flabebe","Flareon","Fletchinder","Fletchling","Floatzel","Floette","Floragato","Florges","Flygon","Fomantis","Foongus","Forretress","Fraxure","Frigibax","Frillish","Froakie","Frogadier","Froslass","Fuecoco","Furfrou","Furret","Gabite","Gallade","Galvantula","Garbodor","Garchomp","Gardevoir","Gastly","Gastrodon","Genesect","Gengar","Geodude","Gholdengo","Gible","Gigalith","Gimmighoul","Girafarig","Giratina","Glaceon","Glalie","Glameow","Gligar","Gliscor","Gloom","Gogoat","Golbat","Goldeen","Golduck","Golem","Golett","Golisopod","Golurk","Goodra","Goomy","Gorebyss","Gothita","Gothitelle","Gothorita","Gourgeist","Granbull","Graveler","Greedent","Greninja","Grimer","Grimmsnarl","Grookey","Grotle","Groudon","Grovyle","Growlithe","Grubbin","Grumpig","Gulpin","Gumshoos","Gurdurr","Guzzlord","Gyarados","Hakamo-o","Happiny","Hariyama","Hatenna","Hatterene","Hattrem","Haunter","Haxorus","Heatmor","Heatran","Heliolisk","Helioptile","Heracross","Herdier","Hippopotas","Hippowdon","Hitmonchan","Hitmonlee","Hitmontop","Ho-Oh","Honchkrow","Hoothoot","Hoppip","Horsea","Houndoom","Houndour","Huntail","Hydreigon","Hypno","Igglybuff","Illumise","Impidimp","Incineroar","Indeedee","Infernape","Inkay","Ivysaur","Jangmo-o","Jellicent","Jigglypuff","Jirachi","Jolteon","Joltik","Jumpluff","Jynx","Kabuto","Kabutops","Kadabra","Kakuna","Kangaskhan","Karrablast","Kartana","Kecleon","Keldeo","Kingambit","Kingdra","Kingler","Kirlia","Klang","Kleavor","Klink","Klinklang","Koffing","Komala","Kommo-o","Krabby","Kricketot","Kricketune","Krokorok","Krookodile","Kyogre","Kyurem","Lairon","Lampent","Landorus","Lanturn","Lapras","Larvesta","Larvitar","Latias","Latios","Leafeon","Leavanny","Lechonk","Ledian","Ledyba","Lickilicky","Lickitung","Liepard","Lileep","Lilligant","Lillipup","Linoone","Litleo","Litten","Litwick","Lokix","Lombre","Lopunny","Lotad","Loudred","Lucario","Ludicolo","Lugia","Lumineon","Lunatone","Lurantis","Luvdisc","Luxio","Luxray","Lycanroc","Machamp","Machoke","Machop","Magby","Magcargo","Magikarp","Magmar","Magmortar","Magnemite","Magneton","Magnezone","Makuhita","Malamar","Mamoswine","Mandibuzz","Manectric","Mankey","Mantine","Mantyke","Maractus","Mareanie","Mareep","Marill","Marowak","Marshtomp","Masquerain","Maushold","Mawile","Medicham","Meditite","Meganium","Melmetal","Meloetta","Meltan","Meowscarada","Meowstic","Meowth","Mesprit","Metagross","Metang","Metapod","Mew","Mewtwo","Mienfoo","Mienshao","Mightyena","Milotic","Miltank","Mime Jr.","Minccino","Minun","Misdreavus","Mismagius","Moltres","Monferno","Morelull","Morgrem","Morpeko","Mothim","Mr. Mime","Mudbray","Mudkip","Mudsdale","Muk","Munchlax","Munna","Murkrow","Musharna","Natu","Necrozma","Nidoking","Nidoqueen","Nidoran♀","Nidoran♂","Nidorina","Nidorino","Nihilego","Nincada","Ninetales","Ninjask","Noctowl","Noibat","Noivern","Nosepass","Numel","Nuzleaf","Nymble","Obstagoon","Octillery","Oddish","Oinkologne","Omanyte","Omastar","Onix","Oranguru","Oricorio","Oshawott","Overqwil","Pachirisu","Palkia","Palossand","Palpitoad","Pancham","Pangoro","Panpour","Pansage","Pansear","Paras","Parasect","Passimian","Patrat","Pawmi","Pawmo","Pawmot","Pawniard","Pelipper","Perrserker","Persian","Petilil","Phanpy","Phantump","Pheromosa","Pichu","Pidgeot","Pidgeotto","Pidgey","Pidove","Pignite","Pikachu","Pikipek","Piloswine","Pineco","Pinsir","Piplup","Plusle","Politoed","Poliwag","Poliwhirl","Poliwrath","Polteageist","Ponyta","Poochyena","Popplio","Porygon","Porygon-Z","Porygon2","Primarina","Primeape","Prinplup","Probopass","Psyduck","Pumpkaboo","Pupitar","Purrloin","Purugly","Pyroar","Quagsire","Quaquaval","Quaxly","Quaxwell","Quilava","Quilladin","Qwilfish","Raichu","Raikou","Ralts","Rampardos","Rapidash","Raticate","Rattata","Rayquaza","Regice","Regidrago","Regieleki","Regigigas","Regirock","Registeel","Relicanth","Remoraid","Reshiram","Reuniclus","Rhydon","Rhyhorn","Rhyperior","Ribombee","Rillaboom","Riolu","Rockruff","Roggenrola","Rookidee","Roselia","Roserade","Rotom","Rowlet","Rufflet","Runerigus","Sableye","Salamence","Salandit","Salazzle","Samurott","Sandile","Sandshrew","Sandslash","Sandygast","Sawk","Sawsbuck","Scatterbug","Sceptile","Scizor","Scolipede","Scrafty","Scraggy","Scyther","Seadra","Seaking","Sealeo","Seedot","Seel","Seismitoad","Sentret","Serperior","Servine","Seviper","Sewaddle","Sharpedo","Shaymin","Shedinja","Shelgon","Shellder","Shellos","Shelmet","Shieldon","Shiftry","Shiinotic","Shinx","Shroomish","Shuckle","Shuppet","Sigilyph","Silcoon","Simipour","Simisage","Simisear","Sinistea","Sirfetch’d","Skarmory","Skeledirge","Skiddo","Skiploom","Skitty","Skorupi","Skrelp","Skuntank","Skwovet","Slaking","Slakoth","Sliggoo","Slowbro","Slowking","Slowpoke","Slugma","Slurpuff","Smeargle","Smoliv","Smoochum","Sneasel","Sneasler","Snivy","Snorlax","Snorunt","Snover","Snubbull","Solosis","Solrock","Spearow","Spewpa","Spheal","Spinarak","Spinda","Spiritomb","Spoink","Sprigatito","Spritzee","Squirtle","Stakataka","Stantler","Staraptor","Staravia","Starly","Starmie","Staryu","Steelix","Steenee","Stoutland","Stufful","Stunfisk","Stunky","Sudowoodo","Suicune","Sunflora","Sunkern","Surskit","Swablu","Swadloon","Swalot","Swampert","Swanna","Swellow","Swinub","Swirlix","Swoobat","Sylveon","Tadbulb","Taillow","Talonflame","Tandemaus","Tangela","Tangrowth","Tapu Bulu","Tapu Fini","Tapu Koko","Tapu Lele","Tauros","Teddiursa","Tentacool","Tentacruel","Tepig","Terrakion","Throh","Thundurus","Thwackey","Timburr","Tinkatink","Tinkaton","Tinkatuff","Tirtouga","Togedemaru","Togekiss","Togepi","Togetic","Torchic","Torkoal","Tornadus","Torracat","Torterra","Totodile","Toucannon","Toxapex","Toxel","Toxicroak","Toxtricity","Tranquill","Trapinch","Treecko","Trevenant","Tropius","Trubbish","Trumbeak","Tsareena","Turtonator","Turtwig","Tympole","Tynamo","Typhlosion","Tyranitar","Tyrantrum","Tyrogue","Tyrunt","Umbreon","Unfezant","Unown","Ursaring","Uxie","Vanillish","Vanillite","Vanilluxe","Vaporeon","Venipede","Venomoth","Venonat","Venusaur","Vespiquen","Vibrava","Victreebel","Vigoroth","Vikavolt","Vileplume","Virizion","Vivillon","Volbeat","Volcarona","Voltorb","Vullaby","Vulpix","Wailmer","Wailord","Walrein","Wartortle","Watchog","Weavile","Weedle","Weepinbell","Weezing","Whimsicott","Whirlipede","Whiscash","Whismur","Wigglytuff","Wiglett","Wimpod","Wingull","Wobbuffet","Woobat","Wooloo","Wooper","Wormadam","Wugtrio","Wurmple","Wynaut","Wyrdeer","Xatu","Xerneas","Xurkitree","Yamask","Yanma","Yanmega","Yungoos","Yveltal","Zacian","Zamazenta","Zangoose","Zapdos","Zebstrika","Zekrom","Zigzagoon","Zoroark","Zorua","Zubat","Zweilous"]} diff --git a/index.html b/index.html index db080bc..5bd3de7 100644 --- a/index.html +++ b/index.html @@ -5,271 +5,800 @@ Event Description Generator - + + + + + + + - - - -
-

Event Description Generator

+
+
+

Observation Deck / Sundance DeGen

+

Event Description Generator

+
+ + + CP + shiny data · loading + +
+
-
- - - - - - - - -
+
+
+

Event Type

+
+
+
+ + + + + + + + +
+
+
-
-

Bulk Import

-

Paste a markdown table of events to generate all descriptions at once.

-
- - - - -
- -
- -
-
+
+
+

Bulk Import

+
+
+

Paste a markdown table of events to generate all descriptions at once.

+
+ + + + +
+ + +
+ +
+
+
- '); } - // Add input listeners for Pokémon fields (replaces inline oninput) + // Build the whole block, then assign once — appending in the loop reparses + // everything already rendered on every iteration. + dynamicInputs.innerHTML = sections.join(''); + dynamicInputs.querySelectorAll('input[id^="pokemon-"]').forEach(input => { - const index = parseInt(input.id.split('-')[1]); - input.addEventListener('input', () => handlePokemonInput(index)); + const index = Number(input.id.split('-')[1]); + attachAutocomplete(input, index); }); - // Add real-time validation for required fields - dynamicInputs.querySelectorAll("input[required], select[required]").forEach(input => { - input.addEventListener("input", () => { - input.style.borderColor = input.value ? "" : "var(--error-red)"; - if (input.id.startsWith("pokemon-")) { - const index = parseInt(input.id.split("-")[1]); - const value = input.value.toLowerCase(); - const suggestions = pokemonList - .filter(p => p.toLowerCase().startsWith(value)) - .slice(0, 5); - showSuggestions(input, suggestions, index); - } - }); - - // Hide suggestions on blur - input.addEventListener("blur", () => { - const suggestionBox = document.getElementById(`suggestions-${input.id.split("-")[1]}`); - if (suggestionBox) { - setTimeout(() => suggestionBox.style.display = "none", 200); - } + dynamicInputs.querySelectorAll('input[required], select[required]').forEach(field => { + field.addEventListener('input', () => { + field.style.borderColor = field.value ? '' : 'var(--down)'; }); }); } - // Generate descriptions (now with dynamic location) + // ---------------------------------------------------------- rendering ---- + + function descriptionCard({ imageUrl, name, text }) { + const image = imageUrl + ? `${escapeHTML(name)}` + : '

No image

'; + + return `
+ ${image} + + + +
`; + } + + const renderOutput = parts => { + output.innerHTML = `

Generated Descriptions

${parts.join('')}`; + }; + + // -------------------------------------------------- manual generation ---- + async function generateDescriptions() { - const num = parseInt(numDescriptionsInput.value); + clearMessages(formMessages); + + const num = parseInt(numDescriptionsInput.value, 10); if (isNaN(num) || num < 1) { - alert("Please enter a valid number of descriptions."); - return; + return showMessages(formMessages, notice('Check', 'Enter how many descriptions you need.', 'error')); } - // Get dynamic location - const locationType = document.querySelector('input[name="location-type"]:checked')?.value; - let location = "Sundance Park"; - if (locationType === "custom") { - location = customLocationInput.value.trim(); - if (!location) { - alert("Please enter a custom location."); - return; - } + const custom = chosenLocation('location-type', customLocationInput); + if (custom === '') { + return showMessages(formMessages, notice('Check', 'Enter a custom location, or switch back to Sundance Park.', 'error')); } const config = eventConfig[selectedEventType]; - - // Collect and validate all form data first const items = []; + const problems = []; + for (let i = 0; i < num; i++) { const pokemon = document.getElementById(`pokemon-${i}`)?.value?.trim(); - const dateInput = document.getElementById(`date-${i}`)?.value; - const shinyAvailable = document.getElementById(`shiny-${i}`)?.value === "yes"; + const dateValue = document.getElementById(`date-${i}`)?.value; - if (!pokemon || !dateInput) { - alert(`Missing required fields for description ${i + 1}.`); - return; + if (!pokemon || !dateValue) { + problems.push(notice('Check', `Description ${i + 1} is missing a Pokémon or a date.`, 'error')); + continue; } - - const formattedDate = formatDate(dateInput); + const formattedDate = formatDate(dateValue); if (!formattedDate) { - alert(`Invalid or past date for description ${i + 1}.`); - return; + problems.push(notice('Check', `Description ${i + 1} has a date in the past.`, 'error')); + continue; } items.push({ pokemon, formattedDate, - shinyAvailable, - bonuses: config.bonuses.length ? collectBonuses(i, config.maxBonuses) : "", - hundo: document.getElementById(`hundo-${i}`)?.value?.trim() || "", - whundo: document.getElementById(`whundo-${i}`)?.value?.trim() || "", - attack: document.getElementById(`attack-${i}`)?.value?.trim() || "" + shinyAvailable: document.getElementById(`shiny-${i}`)?.value === 'yes', + bonuses: collectBonuses(i, config), + hundo: document.getElementById(`hundo-${i}`)?.value?.trim() || '', + whundo: document.getElementById(`whundo-${i}`)?.value?.trim() || '', + attack: document.getElementById(`attack-${i}`)?.value?.trim() || '' }); } - // Fetch all images in parallel - const imageUrls = await Promise.all(items.map(({ pokemon }) => fetchPokemonImage(pokemon))); - - const descriptions = ["

Generated Descriptions

"]; - for (let i = 0; i < items.length; i++) { - const { pokemon, formattedDate, shinyAvailable, bonuses, hundo, whundo, attack } = items[i]; - const imageUrl = imageUrls[i]; - const imageTag = imageUrl ? `${pokemon} image` : "

No image available

"; - - const shinyText = shinyAvailable ? constants.shinyText : ""; - let eventText = `from ${config.time} ${constants.eventEmojis}\n\n`; - - if (bonuses) { - const bonusHeader = bonuses.split("\n").length > 1 ? constants.bonusHeaderMultiple : constants.bonusHeaderSingle; - eventText += `${bonusHeader}\n${bonuses}\n\n`; - } - - if (selectedEventType === "Raid Hour" || selectedEventType === "Raid Day") { - eventText += `💯 - ${hundo} / WB - ${whundo}\n\n`; - } - if (selectedEventType === "Max Battles") { - eventText += `💯 - ${hundo}\n\n`; - } - if (config.specialFields.includes("attack") && attack) { - eventText += `Evolve for featured attack: ${attack}\n\n`; - } + // Report every problem at once rather than stopping at the first. + if (problems.length) showMessages(formMessages, problems.join('')); + if (!items.length) return; - descriptions.push(` -
- ${imageTag} - - - Copied! -
- `); - } + const images = await Promise.all(items.map(item => fetchPokemonImage(item.pokemon))); - output.innerHTML = descriptions.join(""); + renderOutput(items.map((item, i) => descriptionCard({ + imageUrl: images[i], + name: item.pokemon, + text: renderDescription({ ...item, eventType: selectedEventType, location: custom ?? undefined }) + }))); } - // Collect bonuses - function collectBonuses(index, numBonuses) { + function collectBonuses(index, config) { + if (!config.bonuses.length) return []; const bonuses = []; - for (let j = 1; j <= numBonuses; j++) { - const bonus = document.getElementById(`bonus${j}-${index}`)?.value; - if (bonus && bonus !== "None") bonuses.push(`- ${bonus}`); + for (let j = 1; j <= config.maxBonuses; j++) { + const value = document.getElementById(`bonus${j}-${index}`)?.value; + if (value && value !== 'None') bonuses.push(value); } - return bonuses.join("\n"); + return bonuses; } - // Format date - function formatDate(inputDate) { - const today = new Date(); - today.setHours(0, 0, 0, 0); - const date = new Date(inputDate + 'T00:00:00'); - if (date < today) return null; - const options = { month: "long", day: "numeric" }; - const day = date.getDate(); - const suffix = getDaySuffix(day); - return date.toLocaleDateString("en-US", options).replace(day, `${day}${suffix}`); - } + // ---------------------------------------------------- bulk generation ---- - // Get day suffix - function getDaySuffix(day) { - if (day >= 11 && day <= 13) return "th"; - switch (day % 10) { - case 1: return "st"; - case 2: return "nd"; - case 3: return "rd"; - default: return "th"; - } - } + async function generateBulkDescriptions() { + clearMessages(bulkMessages); - // ---- Bulk Import Helpers ---- - - function parseBulkTable(text) { - const events = []; - for (const line of text.trim().split('\n')) { - const trimmed = line.trim(); - if (!trimmed.startsWith('|')) continue; - if (/^\|[-\s|:]+\|$/.test(trimmed)) continue; // separator row - const cols = trimmed.split('|').map(c => c.trim()).filter((_, i, a) => i > 0 && i < a.length - 1); - if (cols.length < 3) continue; - if (/event name/i.test(cols[0])) continue; // header row - const [rawName, details, date] = cols; - const name = rawName.replace(/\*\*/g, '').replace(/^[^\w]+/, '').trim(); - if (name && details && date) events.push({ name, details, date }); + const text = bulkInput.value.trim(); + if (!text) { + return showMessages(bulkMessages, notice('Check', 'Paste an event list first.', 'error')); } - return events; - } - function mapEventTypeFromName(name) { - const n = name.toLowerCase(); - if (n.includes('community day classic')) return 'Community Day Classic'; - if (n.includes('community day')) return 'Community Day'; - if (n.includes('hatch day')) return 'Hatch Day'; - if (n.includes('raid day')) return 'Raid Day'; - if (n.includes('raid hour')) return 'Raid Hour'; - if (n.includes('spotlight hour')) return 'Spotlight Hour'; - if (n.includes('research day')) return 'Research Day'; - if (n.includes('max battle')) return 'Max Battles'; - return null; - } + const events = parseBulkTable(text); + if (!events.length) { + return showMessages(bulkMessages, notice('Check', 'No events found — the input needs to be a markdown table with Event Name, Details and Date columns.', 'error')); + } - function extractPokemonFromEvent(name) { - const dashMatch = name.match(/[–—-]\s*(.+)$/); - if (dashMatch) return dashMatch[1].trim(); - const typePatterns = [ - 'Community Day Classic', 'Community Day', 'Hatch Day', - 'Raid Day', 'Raid Hour', 'Spotlight Hour', 'Research Day', 'Max Battle Day', 'Max Battles' - ]; - for (const pattern of typePatterns) { - const idx = name.toLowerCase().indexOf(pattern.toLowerCase()); - if (idx > 0) { - return name.substring(0, idx).replace(/^Replay:\s*/i, '').trim(); - } + const custom = chosenLocation('bulk-location-type', bulkCustomLocation); + if (custom === '') { + return showMessages(bulkMessages, notice('Check', 'Enter a custom location, or switch back to Sundance Park.', 'error')); } - return ''; - } - function parseDateString(dateStr) { - const monthMap = { - jan:1, january:1, feb:2, february:2, mar:3, march:3, - apr:4, april:4, may:5, jun:6, june:6, jul:7, july:7, - aug:8, august:8, sep:9, september:9, oct:10, october:10, - nov:11, november:11, dec:12, december:12 - }; - const match = dateStr.match(/([A-Za-z]+)\s+(\d+)/); - if (!match) return null; - const month = monthMap[match[1].toLowerCase()]; - const day = parseInt(match[2]); - if (!month || !day) return null; - const now = new Date(); - let year = now.getFullYear(); - if (month < now.getMonth() + 1) year++; - return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`; - } + const rows = events.map(event => parseEventRow(event)); + const skipped = rows.filter(row => !row.ok).map(row => + notice('Skipped', `${escapeHTML(row.reason)} ${escapeHTML(row.event.name)}` + + (row.detail ? ` (${escapeHTML(row.detail)})` : '')) + ); + const usable = rows.filter(row => row.ok); - function extractBonusesFromDetails(eventType, details) { - const config = eventConfig[eventType]; - if (!config || !config.bonuses.length) return []; - const found = []; - const patterns = [ - { re: /3[x×]\s*stardust/i, bonus: '3x Catch Stardust' }, - { re: /2[x×]\s*catch xp\s*[&]\s*stardust|2[x×]\s*xp\s*[&]\s*stardust/i, bonus: '2x Catch XP & Stardust' }, - { re: /2[x×]\s*catch xp|2[x×]\s*xp(?!\s*[&])/i, bonus: '2x Catch XP' }, - { re: /2[x×]\s*catch stardust|2[x×]\s*stardust/i, bonus: '2x Catch Stardust' }, - { re: /2[x×]\s*candy\s*[&]\s*xl|2[x×]\s*candy.*xl/i, bonus: '2x Catch Candy & XL Chance' }, - { re: /2[x×]\s*catch candy|2[x×]\s*candy(?!\s*[&xl])/i, bonus: '2x Catch Candy' }, - { re: /2[x×]\s*transfer candy/i, bonus: '2x Transfer Candy' }, - { re: /2[x×]\s*evolution xp|2[x×]\s*evolv/i, bonus: '2x Evolution XP' }, - { re: /[¼]|1\/4\s*hatch/i, bonus: '1/4 Hatch Distance' }, - { re: /[½]|1\/2\s*hatch/i, bonus: '1/2 Hatch Distance' }, - { re: /3\s*hr\s*lures?/i, bonus: '3 HR Lures / Incense' }, - { re: /boosted shiny|increased shiny/i, bonus: 'Increased shiny chance' }, - { re: /5\s*photobomb/i, bonus: '5 Photobomb Encounters' }, - { re: /extra special trade|special trade/i, bonus: 'Extra Special Trade' }, - { re: /free raid pass/i, bonus: '5 free raid passes by spinning gyms, 6 total' }, - { re: /remote raid/i, bonus: 'Remote raids increased to 20' }, - { re: /1\.5[x×]\s*xp from raids/i, bonus: '1.5x XP from Raids' }, - { re: /extra raid bonus/i, bonus: 'Extra Raid Bonus' }, - { re: /1\/4.*adventure|adventure.*1\/4/i, bonus: '1/4 Adventure Distance for MP' }, - { re: /mp.*1600|1600.*mp/i, bonus: 'MP Collection raised to 1600' }, - { re: /8[x×].*mp|8[x×].*power spot/i, bonus: '8x MP from Power Spots' }, - { re: /2[x×].*mp.*explor/i, bonus: '2x MP for Exploring' }, - ]; - for (const { re, bonus } of patterns) { - if (re.test(details) && config.bonuses.includes(bonus) && !found.includes(bonus)) { - found.push(bonus); - if (found.length >= config.maxBonuses) break; - } - } - return found; - } + if (skipped.length) showMessages(bulkMessages, skipped.join('')); + if (!usable.length) return; - function extractAttackFromDetails(details) { - const match = details.match(/(?:gets|learns)\s+([A-Z][A-Za-z\s]+?)(?:[,.]|$)/); - return match ? match[1].trim() : ''; - } + const [images, cps, shinyFlags] = await Promise.all([ + Promise.all(usable.map(row => row.pokemon ? fetchPokemonImage(row.pokemon) : null)), + Promise.all(usable.map(row => row.needsCP ? getCatchCP(row.pokemon) : null)), + Promise.all(usable.map(row => row.needsShinyCheck ? isShinyReleased(row.pokemon) : null)) + ]); - function isShinyInDetails(details) { - return /shiny/i.test(details); - } + renderOutput(usable.map((row, i) => { + const cp = cps[i]; + return descriptionCard({ + imageUrl: images[i], + name: row.displayName, + text: renderDescription({ + eventType: row.eventType, + pokemon: row.displayName, + location: custom ?? undefined, + formattedDate: row.formattedDate, + bonuses: row.bonuses, + hundo: cp?.hundo ?? '', + whundo: cp?.whundo ?? '', + attack: row.attack, + // The released-shiny list beats keyword-spotting; fall back if it failed. + shinyAvailable: shinyFlags[i] !== null ? shinyFlags[i] : row.shinyFromText + }) + }); + })); - function lookupCP(pokemonName) { - const key = Object.keys(pokemonCPData).find(k => k.toLowerCase() === pokemonName.toLowerCase()); - return key ? pokemonCPData[key] : null; + output.scrollIntoView({ behavior: 'smooth' }); } - async function generateBulkDescriptions() { - const input = document.getElementById("bulk-input").value.trim(); - if (!input) { - alert("Please paste an event list."); - return; - } - const events = parseBulkTable(input); - if (!events.length) { - alert("No events found. Make sure the input is a markdown table."); - return; - } + // ------------------------------------------------------------- events ---- + + Object.keys(eventConfig).forEach(name => { + const button = document.getElementById(`${name.toLowerCase().replace(/ /g, '-')}-btn`); + if (!button) return; + button.addEventListener('click', () => { + selectedEventType = name; + document.querySelectorAll('.event-btn').forEach(btn => + btn.classList.toggle('is-active', btn === button)); + eventForm.style.display = 'block'; + numDescriptionsInput.value = '1'; + addInputs(1); + clearMessages(formMessages); + renderOutput([]); + }); + }); - const bulkLocationType = document.querySelector('input[name="bulk-location-type"]:checked')?.value; - let location = "Sundance Park"; - if (bulkLocationType === "custom") { - location = document.getElementById("bulk-custom-location").value.trim(); - if (!location) { - alert("Please enter a custom location."); - return; - } + numDescriptionsInput.addEventListener('input', debounce(() => { + let num = parseInt(numDescriptionsInput.value, 10); + if (num > MAX_DESCRIPTIONS) { + num = MAX_DESCRIPTIONS; + numDescriptionsInput.value = String(MAX_DESCRIPTIONS); } + if (num > 0) addInputs(num); + else dynamicInputs.innerHTML = ''; + }, 300)); - // Parse all events synchronously, collect warnings for unparseable rows - const parsed = []; - const warnings = []; - for (const event of events) { - const eventType = mapEventTypeFromName(event.name); - if (!eventType) { - warnings.push(`

⚠️ Could not identify event type for: ${event.name}

`); - continue; - } - const config = eventConfig[eventType]; - const pokemon = extractPokemonFromEvent(event.name); - const dateStr = parseDateString(event.date); - const formattedDate = dateStr ? formatDate(dateStr) : null; - if (!formattedDate) { - warnings.push(`

⚠️ Could not parse date for: ${event.name} (${event.date})

`); - continue; - } - const localCP = pokemon ? lookupCP(pokemon) : null; - parsed.push({ - eventType, config, pokemon, formattedDate, - originalName: event.name, - shinyAvailable: isShinyInDetails(event.details), - bonuses: extractBonusesFromDetails(eventType, event.details), - localCP, - needsAPICP: !localCP && pokemon && (eventType === 'Raid Hour' || eventType === 'Raid Day'), - attack: config.specialFields.includes('attack') ? extractAttackFromDetails(event.details) : '' - }); - } + document.getElementById('generate-btn').addEventListener('click', debounce(generateDescriptions, 300)); + document.getElementById('bulk-generate-btn').addEventListener('click', debounce(generateBulkDescriptions, 300)); - // Fetch all images, missing CPs, and shiny status in parallel - const [imageUrls, apiCPs, shinyResults] = await Promise.all([ - Promise.all(parsed.map(p => p.pokemon ? fetchPokemonImage(p.pokemon) : Promise.resolve(null))), - Promise.all(parsed.map(p => p.needsAPICP ? fetchPokemonGOCP(p.pokemon) : Promise.resolve(null))), - Promise.all(parsed.map(p => p.pokemon ? isPokemonShinyReleased(p.pokemon) : Promise.resolve(null))) - ]); + document.getElementById('reset-btn').addEventListener('click', () => { + eventForm.reset(); + dynamicInputs.innerHTML = ''; + eventForm.style.display = 'none'; + selectedEventType = ''; + document.querySelectorAll('.event-btn').forEach(btn => btn.classList.remove('is-active')); + syncLocationVisibility('location-type', customLocationInput); + clearMessages(formMessages); + renderOutput([]); + }); - const descriptions = ["

Generated Descriptions

", ...warnings]; - for (let i = 0; i < parsed.length; i++) { - const { eventType, config, pokemon, formattedDate, originalName, bonuses, localCP, attack } = parsed[i]; - // API shiny check takes priority; fall back to text detection if API failed (null) - const shinyAvailable = shinyResults[i] !== null ? shinyResults[i] : parsed[i].shinyAvailable; - const cpData = localCP || apiCPs[i]; - const hundo = cpData?.hundo ?? ''; - const whundo = cpData?.whundo ?? ''; - - const imageUrl = imageUrls[i]; - const imageTag = imageUrl - ? `${pokemon} image` - : "

No image available

"; - - const shinyText = shinyAvailable ? constants.shinyText : ''; - const displayName = pokemon || originalName; - let eventText = `from ${config.time} ${constants.eventEmojis}\n\n`; - - if (bonuses.length) { - const bonusHeader = bonuses.length > 1 ? constants.bonusHeaderMultiple : constants.bonusHeaderSingle; - eventText += `${bonusHeader}\n${bonuses.map(b => `- ${b}`).join('\n')}\n\n`; - } - if (eventType === 'Raid Hour' || eventType === 'Raid Day') { - eventText += `💯 - ${hundo} / WB - ${whundo}\n\n`; - } - if (eventType === 'Max Battles') { - eventText += `💯 - ${hundo}\n\n`; - } - if (attack) { - eventText += `Evolve for featured attack: ${attack}\n\n`; - } + document.querySelectorAll('input[name="location-type"]').forEach(radio => { + radio.addEventListener('change', () => { + syncLocationVisibility('location-type', customLocationInput); + persist({ locationType: radio.value, customLocation: customLocationInput.value }); + }); + }); - descriptions.push(` -
- ${imageTag} - - - Copied! -
- `); - } + document.querySelectorAll('input[name="bulk-location-type"]').forEach(radio => { + radio.addEventListener('change', () => { + syncLocationVisibility('bulk-location-type', bulkCustomLocation); + persist({ bulkLocationType: radio.value, bulkCustomLocation: bulkCustomLocation.value }); + }); + }); - output.innerHTML = descriptions.join(''); - output.scrollIntoView({ behavior: 'smooth' }); - } + bulkInput.addEventListener('input', debounce(() => persist({ bulkInput: bulkInput.value }), 400)); + bulkCustomLocation.addEventListener('input', debounce(() => persist({ bulkCustomLocation: bulkCustomLocation.value }), 400)); + customLocationInput.addEventListener('input', debounce(() => persist({ customLocation: customLocationInput.value }), 400)); - // Copy to clipboard - window.copyToClipboard = async function (button) { - const textarea = button.previousElementSibling; - const feedback = button.nextElementSibling; - - if (textarea) { - try { - await navigator.clipboard.writeText(textarea.value); - feedback.textContent = "Copied!"; - feedback.style.color = "green"; - feedback.style.display = "inline"; - feedback.addEventListener("click", () => feedback.style.display = "none", { once: true }); - setTimeout(() => feedback.style.display = "none", 5000); - } catch (err) { - feedback.textContent = "Copy failed!"; - feedback.style.color = "var(--error-red)"; - feedback.style.display = "inline"; - feedback.addEventListener("click", () => feedback.style.display = "none", { once: true }); - setTimeout(() => feedback.style.display = "none", 5000); - console.error("Failed to copy:", err); - } + // One delegated handler for every Copy button, present and future. + output.addEventListener('click', async event => { + const button = event.target.closest('[data-copy]'); + if (!button) return; + + const card = button.closest('.description'); + const textarea = card?.querySelector('textarea'); + const feedback = card?.querySelector('.copy-feedback'); + if (!textarea || !feedback) return; + + try { + await navigator.clipboard.writeText(textarea.value); + feedback.textContent = 'Copied'; + feedback.style.color = 'var(--live)'; + } catch (error) { + feedback.textContent = 'Copy failed'; + feedback.style.color = 'var(--down)'; + console.error('Failed to copy:', error); } - }; -}); \ No newline at end of file + feedback.style.display = 'inline'; + setTimeout(() => { feedback.style.display = 'none'; }, 5000); + }); + + restore(); +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..241ad9e --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "sundance-dgen", + "version": "3.0.0", + "private": true, + "description": "Event description generator for Pokémon GO meetups on Campfire", + "type": "module", + "scripts": { + "test": "node --test", + "refresh-data": "node scripts/refresh-data.js" + }, + "engines": { + "node": ">=20" + } +} diff --git a/parse.js b/parse.js new file mode 100644 index 0000000..17a387e --- /dev/null +++ b/parse.js @@ -0,0 +1,333 @@ +/** + * Pure functions: parsing, formatting and rendering. + * + * Nothing in this file touches the DOM, the network, or the clock unless a + * value is passed in — which is what makes it testable. See test/parse.test.js. + */ + +import { eventConfig, constants, eventTypeAliases, bonusPatterns, CPM } from './config.js'; +import { cpOverrides } from './cpOverrides.js'; + +/** + * Fold a Pokémon name to a comparable key. + * + * Sources punctuate inconsistently — "Ho-Oh" / "Ho Oh", "Exeggutor (Alolan)" / + * "Exeggutor Alola", "Mr. Mime" / "Mr_mime", "Farfetch'd" / "Farfetchd" — so + * apostrophes are dropped and every other separator collapses to one space. + */ +export function normalizeName(name) { + return String(name) + .toLowerCase() + .replace(/['’]/g, "") + .replace(/[^a-z0-9]+/g, " ") + .trim(); +} + +export function getDaySuffix(day) { + if (day >= 11 && day <= 13) return "th"; + switch (day % 10) { + case 1: return "st"; + case 2: return "nd"; + case 3: return "rd"; + default: return "th"; + } +} + +/** + * "2026-08-15" -> "August 15th". Returns null for a date in the past, which is + * how callers detect a schedule row that has already happened. + */ +export function formatDate(inputDate, today = new Date()) { + const midnight = new Date(today); + midnight.setHours(0, 0, 0, 0); + const date = new Date(inputDate + "T00:00:00"); + if (isNaN(date.getTime()) || date < midnight) return null; + const day = date.getDate(); + return date + .toLocaleDateString("en-US", { month: "long", day: "numeric" }) + .replace(String(day), `${day}${getDaySuffix(day)}`); +} + +/** Pull `{ name, details, date }` rows out of a pasted markdown table. */ +export function parseBulkTable(text) { + const events = []; + for (const line of String(text).trim().split("\n")) { + const trimmed = line.trim(); + if (!trimmed.startsWith("|")) continue; + if (/^\|[-\s|:]+\|$/.test(trimmed)) continue; // separator row + const cols = trimmed.split("|").map(c => c.trim()).filter((_, i, a) => i > 0 && i < a.length - 1); + if (cols.length < 3) continue; + if (/event name/i.test(cols[0])) continue; // header row + const [rawName, details, date] = cols; + const name = rawName.replace(/\*\*/g, "").replace(/^[^\w]+/, "").trim(); + if (name && details && date) events.push({ name, details, date }); + } + return events; +} + +export function mapEventTypeFromName(name) { + const n = String(name).toLowerCase(); + for (const [fragment, type] of eventTypeAliases) { + if (n.includes(fragment)) return type; + } + return null; +} + +/** + * Separate the Pokémon from the event type in a schedule row's name. + * + * Only en/em dashes, or a hyphen surrounded by whitespace, count as separators. + * A bare hyphen belongs to the Pokémon — Ho-Oh, Porygon-Z, Jangmo-o — and + * splitting on it produced names like "Oh Raid Hour". + */ +export function extractPokemonFromEvent(name) { + const dashMatch = String(name).match(/(?:[–—]|\s-\s)\s*(.+)$/); + if (dashMatch) return dashMatch[1].trim(); + + const typePatterns = [ + "Community Day Classic", "Community Day", "Hatch Day", + "Raid Day", "Raid Hour", "Spotlight Hour", "Research Day", "Max Battle Day", "Max Battles" + ]; + for (const pattern of typePatterns) { + const idx = name.toLowerCase().indexOf(pattern.toLowerCase()); + if (idx > 0) return name.substring(0, idx).replace(/^Replay:\s*/i, "").trim(); + } + return ""; +} + +/** "August 15" -> "2026-08-15", rolling into next year once the month has passed. */ +export function parseDateString(dateStr, now = new Date()) { + const monthMap = { + jan: 1, january: 1, feb: 2, february: 2, mar: 3, march: 3, + apr: 4, april: 4, may: 5, jun: 6, june: 6, jul: 7, july: 7, + aug: 8, august: 8, sep: 9, september: 9, oct: 10, october: 10, + nov: 11, november: 11, dec: 12, december: 12 + }; + const match = String(dateStr).match(/([A-Za-z]+)\s+(\d+)/); + if (!match) return null; + const month = monthMap[match[1].toLowerCase()]; + const day = parseInt(match[2], 10); + if (!month || !day) return null; + let year = now.getFullYear(); + if (month < now.getMonth() + 1) year++; + return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`; +} + +/** Keyword-match the details column against the bonuses this event type allows. */ +export function extractBonusesFromDetails(eventType, details) { + const config = eventConfig[eventType]; + if (!config || !config.bonuses.length) return []; + const found = []; + for (const { re, bonus } of bonusPatterns) { + if (!re.test(details)) continue; + const candidates = Array.isArray(bonus) ? bonus : [bonus]; + const matched = candidates.find(b => config.bonuses.includes(b) && !found.includes(b)); + if (!matched) continue; + found.push(matched); + if (found.length >= config.maxBonuses) break; + } + return found; +} + +export function extractAttackFromDetails(details) { + const match = String(details).match(/(?:gets|learns)\s+([A-Z][A-Za-z\s]+?)(?:[,.]|$)/); + return match ? match[1].trim() : ""; +} + +export function isShinyInDetails(details) { + return /shiny/i.test(String(details)); +} + +/** + * A manually pinned CP, or null when the derived value should be used. + * Normally null — see cpOverrides.js for when an entry is justified. + */ +export function lookupCPOverride(pokemonName) { + const target = normalizeName(pokemonName); + const key = Object.keys(cpOverrides).find(k => normalizeName(k) === target); + return key ? cpOverrides[key] : null; +} + +/** + * Forms that represent the Pokémon you actually catch, best first. + * + * "Normal" covers almost everything. "Hero" exists for Zacian and Zamazenta, + * which have no Normal form — raids give Hero of Many Battles, while their + * Crowned forms need an item and are not catchable. + */ +export const CATCHABLE_FORMS = ["Normal", "Hero"]; + +/** + * Regional form names that sources spell differently. Each entry is indexed + * under every spelling, so "Exeggutor (Alolan)" and "Exeggutor Alola" both hit. + */ +const FORM_SPELLINGS = { + alola: ["alola", "alolan"], + alolan: ["alola", "alolan"], + galar: ["galar", "galarian"], + galarian: ["galar", "galarian"], + hisui: ["hisui", "hisuian"], + hisuian: ["hisui", "hisuian"], + paldea: ["paldea", "paldean"], + paldean: ["paldea", "paldean"] +}; + +/** + * Index base stats for lookup by bare name *and* by name + form. + * + * Two problems this solves: + * + * 1. PoGoAPI lists several forms under one name and does NOT put the base form + * first — Galarian Articuno precedes Normal Articuno, Crowned Sword Zacian + * precedes Hero. "First match wins" returns the wrong variant's stats and a + * visibly wrong catch CP, so the bare name resolves via CATCHABLE_FORMS. + * + * 2. Form-qualified names ("Deoxys Attack", "Giratina (Origin)") have no entry + * of their own — the API stores them as name + form. Registering a combined + * key makes them resolvable without hardcoding their CP anywhere. + * + * @param entries [{ name, atk, def, sta, form? }] + */ +export function indexBaseStats(entries) { + const index = new Map(); + const rank = new Map(); + + // Lower is better; anything unrecognised sorts last but still beats nothing. + const rankOf = form => { + if (form === undefined) return 0; + const i = CATCHABLE_FORMS.indexOf(form); + return i === -1 ? CATCHABLE_FORMS.length : i; + }; + + for (const entry of entries) { + if (!entry?.name) continue; + + const stats = { + base_attack: entry.atk, + base_defense: entry.def, + base_stamina: entry.sta + }; + + // Bare name — contested between forms, so the best-ranked one wins. + const bare = normalizeName(entry.name); + const score = rankOf(entry.form); + if (!index.has(bare) || rank.get(bare) > score) { + index.set(bare, stats); + rank.set(bare, score); + } + + // Form-qualified keys — unambiguous, first writer wins. + if (entry.form === undefined) continue; + const form = normalizeName(entry.form); + for (const spelling of FORM_SPELLINGS[form] ?? [form]) { + const key = `${bare} ${spelling}`; + if (!index.has(key)) index.set(key, stats); + } + } + return index; +} + +/** + * PokéAPI's slug for a Pokémon name, used to look up artwork. + * + * Regional forms are spelled differently there than in schedules — PokéAPI says + * `marowak-alola`, people write "Marowak (Alolan)" — and punctuation has to go, + * or the request 400s. + */ +const SLUG_FORM_ALIASES = { + alolan: "alola", + galarian: "galar", + hisuian: "hisui", + paldean: "paldea" +}; + +export function pokeApiSlug(name) { + const parts = normalizeName(name).split(" ").filter(Boolean); + if (!parts.length) return ""; + const last = parts.length - 1; + parts[last] = SLUG_FORM_ALIASES[parts[last]] ?? parts[last]; + return parts.join("-"); +} + +/** Catch CP from raw Pokémon GO base stats, at both raid catch levels. */ +export function calcCatchCP({ base_attack, base_defense, base_stamina }) { + const at = cpm => Math.floor( + (base_attack + 15) * Math.sqrt(base_defense + 15) * Math.sqrt(base_stamina + 15) * cpm * cpm / 10 + ); + return { hundo: at(CPM.level20), whundo: at(CPM.level25) }; +} + +/** + * The one place the description text is assembled. Both the manual builder and + * the bulk importer call this, so the two can no longer drift apart. + * + * Which optional lines appear is derived from the event type's `specialFields`, + * not from its name. + */ +export function renderDescription({ + eventType, + pokemon, + location = constants.defaultLocation, + formattedDate, + bonuses = [], + hundo = "", + whundo = "", + attack = "", + shinyAvailable = false +}) { + const config = eventConfig[eventType]; + if (!config) throw new Error(`Unknown event type: ${eventType}`); + + let body = `from ${config.time} ${constants.eventEmojis}\n\n`; + + if (bonuses.length) { + const header = bonuses.length > 1 ? constants.bonusHeaderMultiple : constants.bonusHeaderSingle; + body += `${header}\n${bonuses.map(b => `- ${b}`).join("\n")}\n\n`; + } + if (config.specialFields.includes("hundo")) { + body += config.specialFields.includes("whundo") + ? `💯 - ${hundo} / WB - ${whundo}\n\n` + : `💯 - ${hundo}\n\n`; + } + if (config.specialFields.includes("attack") && attack) { + body += `Evolve for featured attack: ${attack}\n\n`; + } + + const shinyText = shinyAvailable ? constants.shinyText : ""; + return `${pokemon} ${eventType}\n` + + `🎈 Join us at ${location} on ${formattedDate} for the ${pokemon} ${eventType} ${body}` + + `${shinyText}${constants.checkInText}`; +} + +/** + * Turn one bulk-table row into everything needed to render it, or an error + * explaining why it can't be rendered. Network-derived values (live CP, shiny + * status, artwork) are layered on by the caller. + */ +export function parseEventRow(event, now = new Date()) { + const eventType = mapEventTypeFromName(event.name); + if (!eventType) { + return { ok: false, reason: `Could not identify event type for`, event }; + } + const config = eventConfig[eventType]; + const pokemon = extractPokemonFromEvent(event.name); + const dateStr = parseDateString(event.date, now); + const formattedDate = dateStr ? formatDate(dateStr, now) : null; + if (!formattedDate) { + return { ok: false, reason: `Could not parse date for`, event, detail: event.date }; + } + return { + ok: true, + eventType, + pokemon, + displayName: pokemon || event.name, + formattedDate, + shinyFromText: isShinyInDetails(event.details), + bonuses: extractBonusesFromDetails(eventType, event.details), + // CP is resolved by the caller (it needs the data layer); this only says + // whether this event type shows a CP line at all. + needsCP: !!pokemon && config.specialFields.includes("hundo"), + needsShinyCheck: !!pokemon, + attack: config.specialFields.includes("attack") ? extractAttackFromDetails(event.details) : "" + }; +} diff --git a/pokemonCPData.js b/pokemonCPData.js deleted file mode 100644 index 5544226..0000000 --- a/pokemonCPData.js +++ /dev/null @@ -1,179 +0,0 @@ -const pokemonCPData = { - "Absol": { hundo: 1448, whundo: 1810 }, - "Aerodactyl": { hundo: 1590, whundo: 1988 }, - "Aggron": { hundo: 1719, whundo: 2149 }, - "Alakazam": { hundo: 1557, whundo: 1946 }, - "Altaria": { hundo: 1144, whundo: 1430 }, - "Ampharos": { hundo: 1542, whundo: 1927 }, - "Anorith": { hundo: 869, whundo: 1086 }, - "Arcanine": { hundo: 1732, whundo: 2165 }, - "Articuno": { hundo: 1743, whundo: 2179 }, - "Azelf": { hundo: 1834, whundo: 2293 }, - "Azumarill": { hundo: 907, whundo: 1134 }, - "Bagon": { hundo: 660, whundo: 825 }, - "Banette": { hundo: 1327, whundo: 1659 }, - "Beedrill": { hundo: 1058, whundo: 1322 }, - "Beldum": { hundo: 558, whundo: 697 }, - "Blacephalon": { hundo: 1884, whundo: 2355 }, - "Blastoise": { hundo: 1407, whundo: 1759 }, - "Blaziken": { hundo: 1628, whundo: 2035 }, - "Buizel": { hundo: 604, whundo: 755 }, - "Buzzwole": { hundo: 1977, whundo: 2472 }, - "Celebi": { hundo: 1863, whundo: 2329 }, - "Chansey": { hundo: 718, whundo: 897 }, - "Charizard": { hundo: 1645, whundo: 2056 }, - "Chikorita": { hundo: 537, whundo: 671 }, - "Claydol": { hundo: 1127, whundo: 1409 }, - "Cobalion": { hundo: 1727, whundo: 2159 }, - "Cresselia": { hundo: 1633, whundo: 2041 }, - "Croagunk": { hundo: 543, whundo: 679 }, - "Cubone": { hundo: 670, whundo: 837 }, - "Cyndaquil": { hundo: 558, whundo: 697 }, - "Darkrai": { hundo: 2136, whundo: 2670 }, - "Deoxys Attack": { hundo: 1474, whundo: 1842 }, - "Deoxys Defense": { hundo: 1296, whundo: 1620 }, - "Deoxys Normal": { hundo: 1802, whundo: 2253 }, - "Deoxys Speed": { hundo: 1641, whundo: 2051 }, - "Dialga": { hundo: 2306, whundo: 2883 }, - "Dragonite": { hundo: 2165, whundo: 2706 }, - "Dratini": { hundo: 574, whundo: 717 }, - "Drilbur": { hundo: 785, whundo: 981 }, - "Eevee": { hundo: 612, whundo: 765 }, - "Electabuzz": { hundo: 1332, whundo: 1665 }, - "Entei": { hundo: 1984, whundo: 2480 }, - "Exeggutor": { hundo: 1716, whundo: 2145 }, - "Exeggutor (Alolan)": { hundo: 1722, whundo: 2152 }, - "Ferroseed": { hundo: 541, whundo: 676 }, - "Gallade": { hundo: 1498, whundo: 1872 }, - "Garchomp": { hundo: 2265, whundo: 2831 }, - "Gardevoir": { hundo: 1773, whundo: 2216 }, - "Genesect": { hundo: 1916, whundo: 2395 }, - "Gengar": { hundo: 1646, whundo: 2058 }, - "Giratina (Altered)": { hundo: 1931, whundo: 2414 }, - "Giratina (Origin)": { hundo: 2118, whundo: 2648 }, - "Golett": { hundo: 679, whundo: 849 }, - "Groudon": { hundo: 2321, whundo: 2902 }, - "Growlithe": { hundo: 708, whundo: 885 }, - "Guzzlord": { hundo: 1650, whundo: 2062 }, - "Gyarados": { hundo: 1937, whundo: 2421 }, - "Heatran": { hundo: 2145, whundo: 2681 }, - "Heracross": { hundo: 1757, whundo: 2196 }, - "Hitmonchan": { hundo: 1326, whundo: 1658 }, - "Hitmonlee": { hundo: 1473, whundo: 1841 }, - "Ho Oh": { hundo: 2207, whundo: 2759 }, - "Houndoom": { hundo: 1514, whundo: 1893 }, - "Houndour": { hundo: 705, whundo: 881 }, - "Jirachi": { hundo: 1863, whundo: 2329 }, - "Kabuto": { hundo: 783, whundo: 979 }, - "Kabutops": { hundo: 1548, whundo: 1935 }, - "Kartana": { hundo: 2101, whundo: 2626 }, - "Kingler": { hundo: 1616, whundo: 2020 }, - "Kleavor": { hundo: 1874, whundo: 2342 }, - "Klink": { hundo: 548, whundo: 685 }, - "Kyogre": { hundo: 2321, whundo: 2902 }, - "Kyurem": { hundo: 2048, whundo: 2560 }, - "Lapras": { hundo: 1505, whundo: 1881 }, - "Larvitar": { hundo: 595, whundo: 744 }, - "Latias": { hundo: 2006, whundo: 2508 }, - "Latios": { hundo: 2178, whundo: 2723 }, - "Lickitung": { hundo: 806, whundo: 1007 }, - "Lileep": { hundo: 731, whundo: 914 }, - "Litten": { hundo: 581, whundo: 726 }, - "Lugia": { hundo: 2115, whundo: 2644 }, - "Lunala": { hundo: 2310, whundo: 2887 }, - "Machamp": { hundo: 1751, whundo: 2189 }, - "Machop": { hundo: 727, whundo: 909 }, - "Magikarp": { hundo: 156, whundo: 195 }, - "Magmar": { hundo: 1366, whundo: 1708 }, - "Manectric": { hundo: 1336, whundo: 1670 }, - "Mantine": { hundo: 1206, whundo: 1508 }, - "Marowak (Alolan)": { hundo: 1047, whundo: 1309 }, - "Mawile": { hundo: 934, whundo: 1167 }, - "Medicham": { hundo: 816, whundo: 1020 }, - "Mesprit": { hundo: 1748, whundo: 2185 }, - "Metagross": { hundo: 2187, whundo: 2734 }, - "Mew": { hundo: 1840, whundo: 2300 }, - "Mewtwo": { hundo: 2387, whundo: 2984 }, - "Misdreavus": { hundo: 1098, whundo: 1372 }, - "Muk": { hundo: 1571, whundo: 1964 }, - "Necrozma": { hundo: 2104, whundo: 2630 }, - "Nihilego": { hundo: 2256, whundo: 2821 }, - "Ninetales": { hundo: 1302, whundo: 1628 }, - "Omanyte": { hundo: 880, whundo: 1100 }, - "Omastar": { hundo: 1535, whundo: 1919 }, - "Palkia": { hundo: 2288, whundo: 2860 }, - "Pheromosa": { hundo: 1624, whundo: 2030 }, - "Pidgeot": { hundo: 1217, whundo: 1521 }, - "Pinsir": { hundo: 1692, whundo: 2115 }, - "Ponyta": { hundo: 970, whundo: 1212 }, - "Psyduck": { hundo: 686, whundo: 857 }, - "Raikou": { hundo: 1972, whundo: 2465 }, - "Rayquaza": { hundo: 2191, whundo: 2739 }, - "Regice": { hundo: 1784, whundo: 2230 }, - "Regidrago": { hundo: 1699, whundo: 2124 }, - "Regieleki": { hundo: 1602, whundo: 2002 }, - "Regigigas": { hundo: 2483, whundo: 3104 }, - "Regirock": { hundo: 1784, whundo: 2230 }, - "Registeel": { hundo: 1398, whundo: 1748 }, - "Reshiram": { hundo: 2302, whundo: 2878 }, - "Rhydon": { hundo: 1816, whundo: 2270 }, - "Sableye": { hundo: 866, whundo: 1082 }, - "Salamence": { hundo: 2165, whundo: 2706 }, - "Sceptile": { hundo: 1566, whundo: 1958 }, - "Scizor": { hundo: 1712, whundo: 2140 }, - "Scyther": { hundo: 1547, whundo: 1934 }, - "Sharpedo": { hundo: 1246, whundo: 1558 }, - "Shinx": { hundo: 500, whundo: 625 }, - "Shuckle": { hundo: 231, whundo: 289 }, - "Skarmory": { hundo: 1207, whundo: 1509 }, - "Skorupi": { hundo: 576, whundo: 720 }, - "Slowbro": { hundo: 1458, whundo: 1823 }, - "Sneasel": { hundo: 1172, whundo: 1465 }, - "Snorlax": { hundo: 1843, whundo: 2304 }, - "Solgaleo": { hundo: 2310, whundo: 2887 }, - "Stakataka": { hundo: 1882, whundo: 2353 }, - "Steelix": { hundo: 1388, whundo: 1735 }, - "Suicune": { hundo: 1716, whundo: 2145 }, - "Swablu": { hundo: 462, whundo: 578 }, - "Swampert": { hundo: 1695, whundo: 2119 }, - "Tangela": { hundo: 1278, whundo: 1597 }, - "Tatsugiri": { hundo: 1037, whundo: 1296 }, - "Tentacool": { hundo: 597, whundo: 746 }, - "Terrakion": { hundo: 2113, whundo: 2641 }, - "Thundurus (Incarnate)": { hundo: 1911, whundo: 2389 }, - "Timburr": { hundo: 749, whundo: 936 }, - "Togetic": { hundo: 975, whundo: 1219 }, - "Torkoal": { hundo: 1190, whundo: 1488 }, - "Tornadus (Incarnate)": { hundo: 1911, whundo: 2389 }, - "Totodile": { hundo: 645, whundo: 806 }, - "Tyranitar": { hundo: 2191, whundo: 2739 }, - "Uxie": { hundo: 1446, whundo: 1808 }, - "Venusaur": { hundo: 1556, whundo: 1945 }, - "Virizion": { hundo: 1722, whundo: 2152 }, - "Voltorb": { hundo: 578, whundo: 722 }, - "Wailmer": { hundo: 835, whundo: 1044 }, - "Weezing": { hundo: 1311, whundo: 1639 }, - "Wobbuffet": { hundo: 586, whundo: 732 }, - "Xerneas": { hundo: 2160, whundo: 2700 }, - "Xurkitree": { hundo: 2249, whundo: 2812 }, - "Yveltal": { hundo: 2160, whundo: 2700 }, - "Zacian": { hundo: 2188, whundo: 2735 }, - "Zamazenta": { hundo: 2188, whundo: 2735 }, - "Zekrom": { hundo: 2302, whundo: 2878 }, -}; - -function autoFillCP(index) { - const pokemon = document.getElementById(`pokemon-${index}`).value; - const data = pokemonCPData[pokemon] || pokemonCPData[Object.keys(pokemonCPData).find(key => key.toLowerCase() === pokemon.toLowerCase())]; - const hundoInput = document.getElementById(`hundo-${index}`); - const whundoInput = document.getElementById(`whundo-${index}`); - if (data) { - hundoInput.value = data.hundo; - whundoInput.value = data.whundo; - } else { - hundoInput.value = ""; - whundoInput.value = ""; - } -} - -export { pokemonCPData, autoFillCP }; \ No newline at end of file diff --git a/scripts/refresh-data.js b/scripts/refresh-data.js new file mode 100644 index 0000000..4c62401 --- /dev/null +++ b/scripts/refresh-data.js @@ -0,0 +1,167 @@ +#!/usr/bin/env node +/** + * Refresh the vendored data snapshots in data/. + * + * PoGoAPI is a volunteer project. Fetching it at page load means a description + * can silently lose its CP or shiny line the day that service has a bad hour — + * the worst kind of failure, because nothing looks broken. Committing a trimmed + * snapshot turns that runtime dependency into a repo file; the live API is kept + * only as a fallback for Pokémon released since the last refresh. + * + * npm run refresh-data + * + * Run weekly by .github/workflows/refresh-data.yml. + */ + +import { writeFile, mkdir } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { calcCatchCP, indexBaseStats, normalizeName, CATCHABLE_FORMS } from "../parse.js"; +import { cpOverrides } from "../cpOverrides.js"; + +const DATA_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..", "data"); + +const SOURCES = { + stats: "https://pogoapi.net/api/v1/pokemon_stats.json", + shiny: "https://pogoapi.net/api/v1/shiny_pokemon.json" +}; + +async function getJSON(url) { + const response = await fetch(url, { signal: AbortSignal.timeout(30_000) }); + if (!response.ok) throw new Error(`${response.status} ${response.statusText} for ${url}`); + return response.json(); +} + +/** + * Reduce each entry to a name, its form, and the three stats the CP formula + * needs. + * + * Every form is kept, not just the base one. Form-qualified names like + * "Deoxys Attack" and "Giratina (Origin)" have no entry of their own — the API + * models them as name + form — so dropping the alternates would make them + * unresolvable, and their CP line would silently come out blank. + * + * Choosing *which* form a bare name resolves to is the runtime indexer's job + * (see indexBaseStats and CATCHABLE_FORMS); this only decides what gets stored. + */ +function trimStats(raw) { + const seen = new Set(); + const pokemon = []; + + for (const entry of raw) { + const name = entry.pokemon_name; + if (!name) continue; + const key = `${name}::${entry.form ?? ""}`; + if (seen.has(key)) continue; + seen.add(key); + pokemon.push({ + name, + form: entry.form, + atk: entry.base_attack, + def: entry.base_defense, + sta: entry.base_stamina + }); + } + return pokemon; +} + +function trimShiny(raw) { + const names = new Set(); + for (const entry of Object.values(raw)) { + const name = Array.isArray(entry) ? entry[0]?.name : entry?.name; + if (name) names.add(name); + } + return [...names].sort(); +} + +async function main() { + const generated = new Date().toISOString(); + + const [rawStats, rawShiny] = await Promise.all([ + getJSON(SOURCES.stats), + getJSON(SOURCES.shiny) + ]); + + const pokemon = trimStats(rawStats); + const names = trimShiny(rawShiny); + + if (pokemon.length < 500) throw new Error(`Only ${pokemon.length} Pokémon in stats — refusing to write a truncated snapshot`); + if (names.length < 200) throw new Error(`Only ${names.length} shiny names — refusing to write a truncated snapshot`); + + await mkdir(DATA_DIR, { recursive: true }); + await writeFile( + resolve(DATA_DIR, "pokemon-stats.json"), + JSON.stringify({ generated, source: SOURCES.stats, pokemon }) + "\n" + ); + await writeFile( + resolve(DATA_DIR, "shiny-pokemon.json"), + JSON.stringify({ generated, source: SOURCES.shiny, names }) + "\n" + ); + + console.log(`Wrote ${pokemon.length} Pokémon stats and ${names.length} shiny names (generated ${generated}).`); + auditOverrides(pokemon); + checkResolvable(pokemon); +} + +/** + * CP is derived, so nothing here can go stale — except cpOverrides.js, whose + * values nothing re-checks. Report any override that now agrees with the + * derived value (safe to delete) or that no longer resolves at all. + */ +function auditOverrides(pokemon) { + const entries = Object.entries(cpOverrides); + if (!entries.length) { + console.log("No CP overrides defined — every value is derived from base stats."); + return; + } + + const index = indexBaseStats(pokemon); + const redundant = []; + const unresolvable = []; + + for (const [name, pinned] of entries) { + const stats = index.get(normalizeName(name)); + if (!stats) { + unresolvable.push(name); + continue; + } + const derived = calcCatchCP(stats); + if (derived.hundo === pinned.hundo && derived.whundo === pinned.whundo) redundant.push(name); + } + + console.log(`\n${entries.length} CP override(s) defined.`); + if (redundant.length) { + console.log(` ${redundant.length} now match the derived value and can be deleted: ${redundant.join(", ")}`); + } + if (unresolvable.length) { + console.log(` ${unresolvable.length} name(s) no longer appear in the stats data: ${unresolvable.join(", ")}`); + } +} + +/** + * Spot-check that names the tool is actually asked for still resolve — including + * the form-qualified spellings, which depend on the form-key indexing. + */ +function checkResolvable(pokemon) { + const index = indexBaseStats(pokemon); + const samples = [ + "Charizard", "Ho-Oh", "Mr. Mime", "Deoxys Attack", "Deoxys Speed", + "Exeggutor (Alolan)", "Marowak (Alolan)", "Giratina (Altered)", "Giratina (Origin)", + "Thundurus (Incarnate)", "Tornadus (Incarnate)", "Zacian", "Zamazenta", "Articuno" + ]; + const missing = samples.filter(name => !index.get(normalizeName(name))); + + if (missing.length) { + console.log(`\nWARNING — these names no longer resolve from the stats data: ${missing.join(", ")}`); + console.log("Autocomplete will still offer them, but their CP line will come out blank."); + } else { + console.log(`All ${samples.length} sampled names resolve, form-qualified spellings included.`); + } +} + + +main().catch(error => { + console.error("Data refresh failed:", error.message); + process.exit(1); +}); diff --git a/test/parse.test.js b/test/parse.test.js new file mode 100644 index 0000000..91a90bf --- /dev/null +++ b/test/parse.test.js @@ -0,0 +1,485 @@ +import { test, describe } from "node:test"; +import assert from "node:assert/strict"; + +import { + normalizeName, + getDaySuffix, + formatDate, + parseBulkTable, + mapEventTypeFromName, + extractPokemonFromEvent, + parseDateString, + extractBonusesFromDetails, + extractAttackFromDetails, + isShinyInDetails, + lookupCPOverride, + calcCatchCP, + indexBaseStats, + pokeApiSlug, + renderDescription, + parseEventRow +} from "../parse.js"; + +// A fixed "now" so nothing here depends on the day the suite runs. +const NOW = new Date("2026-08-01T12:00:00"); + +describe("extractPokemonFromEvent", () => { + // Regression: a bare hyphen was treated as a separator, so "Ho-Oh Raid Hour" + // parsed as "Oh Raid Hour" — wrong name, no artwork, no CP. + test("keeps hyphenated Pokémon names intact", () => { + assert.equal(extractPokemonFromEvent("Ho-Oh Raid Hour"), "Ho-Oh"); + assert.equal(extractPokemonFromEvent("Porygon-Z Spotlight Hour"), "Porygon-Z"); + assert.equal(extractPokemonFromEvent("Jangmo-o Community Day"), "Jangmo-o"); + }); + + test("splits on en/em dashes and spaced hyphens", () => { + assert.equal(extractPokemonFromEvent("Raid Day — Charizard"), "Charizard"); + assert.equal(extractPokemonFromEvent("Raid Day – Charizard"), "Charizard"); + assert.equal(extractPokemonFromEvent("Raid Day - Charizard"), "Charizard"); + }); + + test("splits on the event type when there is no dash", () => { + assert.equal(extractPokemonFromEvent("Charizard Raid Day"), "Charizard"); + assert.equal(extractPokemonFromEvent("Mr. Mime Spotlight Hour"), "Mr. Mime"); + }); + + test("strips a Replay: prefix", () => { + assert.equal(extractPokemonFromEvent("Replay: Charmander Community Day"), "Charmander"); + }); + + test("returns empty string when nothing can be extracted", () => { + assert.equal(extractPokemonFromEvent("Mystery Gathering"), ""); + }); +}); + +describe("lookupCPOverride", () => { + // CP is derived from base stats; overrides are the rare manual pin. + test("returns null when nothing is pinned", () => { + assert.equal(lookupCPOverride("Charizard"), null); + assert.equal(lookupCPOverride("Not A Pokemon"), null); + }); + + test("tolerates an empty or missing name", () => { + assert.equal(lookupCPOverride(""), null); + }); +}); + +describe("extractBonusesFromDetails", () => { + // Regression: the pattern mapped only to "Extra Special Trade", which + // Community Day does not declare, so the bonus was silently dropped. + test("resolves the trade bonus to each event type's own spelling", () => { + assert.deepEqual( + extractBonusesFromDetails("Community Day", "+1 special trade and half trade cost"), + ["+1 Special Trade & Half Trade Cost"] + ); + assert.deepEqual( + extractBonusesFromDetails("Community Day Classic", "extra special trade"), + ["Extra Special Trade"] + ); + }); + + // Regression: /3[x×]\s*stardust/ did not allow "Catch" in between. + test("matches 3x Catch Stardust as well as 3x Stardust", () => { + assert.deepEqual(extractBonusesFromDetails("Community Day", "3x Catch Stardust"), ["3x Catch Stardust"]); + assert.deepEqual(extractBonusesFromDetails("Community Day", "3x Stardust"), ["3x Catch Stardust"]); + }); + + test("finds several bonuses in one details cell", () => { + assert.deepEqual( + extractBonusesFromDetails("Community Day", "3x Catch Stardust, +1 special trade and half trade cost, 1/4 hatch distance"), + ["3x Catch Stardust", "1/4 Hatch Distance", "+1 Special Trade & Half Trade Cost"] + ); + }); + + test("never exceeds the event type's maxBonuses", () => { + const bonuses = extractBonusesFromDetails( + "Spotlight Hour", + "2x Catch Stardust, 2x Catch XP, 2x Catch Candy, 2x Transfer Candy, 2x Evolution XP" + ); + assert.equal(bonuses.length, 2); // Spotlight Hour caps at 2 + }); + + test("ignores bonuses the event type does not offer", () => { + assert.deepEqual(extractBonusesFromDetails("Raid Hour", "3x Catch Stardust"), []); + }); + + test("returns empty for an unknown event type", () => { + assert.deepEqual(extractBonusesFromDetails("Nonsense Day", "3x Catch Stardust"), []); + }); +}); + +describe("renderDescription", () => { + // Golden outputs captured from the pre-refactor code, so collapsing the two + // description builders into one provably did not change the wording. + test("raid event: CP line with weather-boosted value", () => { + assert.equal(renderDescription({ + eventType: "Raid Hour", + pokemon: "Ho-Oh", + location: "Sundance Park", + formattedDate: "August 12th", + hundo: 2207, + whundo: 2759, + shinyAvailable: true + }), [ + "Ho-Oh Raid Hour", + "🎈 Join us at Sundance Park on August 12th for the Ho-Oh Raid Hour from 6-7PM 💃☀️🕺", + "", + "💯 - 2207 / WB - 2759", + "", + "If you're lucky, you might encounter a shiny one ✨", + '✅ "Check in" on Campfire when you arrive' + ].join("\n")); + }); + + test("Max Battles: CP line without the weather-boosted value", () => { + const out = renderDescription({ + eventType: "Max Battles", + pokemon: "Charizard", + formattedDate: "August 20th", + hundo: 1645 + }); + assert.match(out, /💯 - 1645\n/); + assert.doesNotMatch(out, /WB -/); + }); + + test("singular vs plural bonus header", () => { + const one = renderDescription({ + eventType: "Spotlight Hour", + pokemon: "Porygon-Z", + formattedDate: "August 13th", + bonuses: ["2x Catch Candy"], + shinyAvailable: true + }); + assert.equal(one, [ + "Porygon-Z Spotlight Hour", + "🎈 Join us at Sundance Park on August 13th for the Porygon-Z Spotlight Hour from 6-7PM 💃☀️🕺", + "", + "————Event Bonus————", + "- 2x Catch Candy", + "", + "If you're lucky, you might encounter a shiny one ✨", + '✅ "Check in" on Campfire when you arrive' + ].join("\n")); + + const many = renderDescription({ + eventType: "Spotlight Hour", + pokemon: "Pikachu", + formattedDate: "August 13th", + bonuses: ["2x Catch Candy", "2x Catch XP"] + }); + assert.match(many, /————Event Bonuses————/); + }); + + test("omits the shiny line when shiny is unavailable", () => { + const out = renderDescription({ + eventType: "Spotlight Hour", + pokemon: "Bidoof", + formattedDate: "August 13th", + shinyAvailable: false + }); + assert.doesNotMatch(out, /shiny one/); + assert.match(out, /Check in/); + }); + + test("featured attack only appears for event types that collect one", () => { + const cd = renderDescription({ + eventType: "Community Day", + pokemon: "Charmander", + formattedDate: "August 16th", + attack: "Blast Burn" + }); + assert.match(cd, /Evolve for featured attack: Blast Burn/); + + // Spotlight Hour has no "attack" special field, so an attack is ignored. + const sh = renderDescription({ + eventType: "Spotlight Hour", + pokemon: "Charmander", + formattedDate: "August 16th", + attack: "Blast Burn" + }); + assert.doesNotMatch(sh, /featured attack/); + }); + + test("defaults the location", () => { + const out = renderDescription({ eventType: "Hatch Day", pokemon: "Togepi", formattedDate: "August 9th" }); + assert.match(out, /Join us at Sundance Park/); + }); + + test("throws on an unknown event type rather than emitting nonsense", () => { + assert.throws(() => renderDescription({ eventType: "Nope", pokemon: "X", formattedDate: "y" }), /Unknown event type/); + }); +}); + +describe("parseBulkTable", () => { + const table = ` +| Event Name | Details | Date | +| --- | --- | --- | +| **Charizard Raid Day** | Boosted shiny | August 15 | +| Ho-Oh Raid Hour | shiny | August 12 | +not a table row +| too | few | +`; + + test("extracts rows, skipping header, separator and non-rows", () => { + const rows = parseBulkTable(table); + assert.equal(rows.length, 2); + assert.deepEqual(rows[0], { name: "Charizard Raid Day", details: "Boosted shiny", date: "August 15" }); + assert.equal(rows[1].name, "Ho-Oh Raid Hour"); + }); + + test("strips bold markers and leading decoration", () => { + assert.equal(parseBulkTable("| **✨ Charizard Raid Day** | x | August 15 |")[0].name, "Charizard Raid Day"); + }); + + test("returns empty for input that is not a table", () => { + assert.deepEqual(parseBulkTable("just some prose"), []); + }); +}); + +describe("parseDateString", () => { + test("resolves a month/day against the current year", () => { + assert.equal(parseDateString("August 15", NOW), "2026-08-15"); + assert.equal(parseDateString("Aug 5", NOW), "2026-08-05"); + }); + + test("rolls into next year once the month has passed", () => { + assert.equal(parseDateString("January 10", NOW), "2027-01-10"); + }); + + test("returns null for unparseable input", () => { + assert.equal(parseDateString("sometime soon", NOW), null); + assert.equal(parseDateString("Smarch 4", NOW), null); + }); +}); + +describe("formatDate", () => { + test("adds the right ordinal suffix", () => { + assert.equal(formatDate("2026-08-01", NOW), "August 1st"); + assert.equal(formatDate("2026-08-02", NOW), "August 2nd"); + assert.equal(formatDate("2026-08-03", NOW), "August 3rd"); + assert.equal(formatDate("2026-08-04", NOW), "August 4th"); + assert.equal(formatDate("2026-08-21", NOW), "August 21st"); + }); + + test("uses 'th' for the 11-13 exceptions", () => { + assert.equal(formatDate("2026-08-11", NOW), "August 11th"); + assert.equal(formatDate("2026-08-12", NOW), "August 12th"); + assert.equal(formatDate("2026-08-13", NOW), "August 13th"); + }); + + test("returns null for a date already past", () => { + assert.equal(formatDate("2026-07-31", NOW), null); + }); + + test("accepts today", () => { + assert.equal(formatDate("2026-08-01", NOW), "August 1st"); + }); + + test("returns null for garbage", () => { + assert.equal(formatDate("not-a-date", NOW), null); + }); +}); + +describe("getDaySuffix", () => { + test("covers the whole month", () => { + const suffixes = Array.from({ length: 31 }, (_, i) => getDaySuffix(i + 1)); + assert.equal(suffixes[0], "st"); // 1 + assert.equal(suffixes[10], "th"); // 11 + assert.equal(suffixes[11], "th"); // 12 + assert.equal(suffixes[12], "th"); // 13 + assert.equal(suffixes[20], "st"); // 21 + assert.equal(suffixes[21], "nd"); // 22 + assert.equal(suffixes[22], "rd"); // 23 + assert.equal(suffixes[30], "st"); // 31 + }); +}); + +describe("indexBaseStats", () => { + // Regression: PoGoAPI lists Galarian Articuno *before* Normal Articuno, so + // "first match wins" returned the regional variant — 2051 CP instead of 1743. + const articuno = [ + { name: "Articuno", atk: 250, def: 197, sta: 207, form: "Galarian" }, + { name: "Articuno", atk: 192, def: 236, sta: 207, form: "Normal" } + ]; + + test("prefers the Normal form even when it is not listed first", () => { + const index = indexBaseStats(articuno); + assert.deepEqual(index.get("articuno"), { base_attack: 192, base_defense: 236, base_stamina: 207 }); + assert.equal(calcCatchCP(index.get("articuno")).hundo, 1743); + }); + + test("keeps the Normal form when it comes first too", () => { + const index = indexBaseStats([...articuno].reverse()); + assert.equal(index.get("articuno").base_attack, 192); + }); + + test("falls back to the first entry when no Normal form exists", () => { + const index = indexBaseStats([ + { name: "Tatsugiri", atk: 1, def: 2, sta: 3, form: "Curly" }, + { name: "Tatsugiri", atk: 9, def: 9, sta: 9, form: "Droopy" } + ]); + assert.equal(index.get("tatsugiri").base_attack, 1); + }); + + test("treats a missing form field as Normal", () => { + const index = indexBaseStats([{ name: "Charizard", atk: 223, def: 173, sta: 186 }]); + assert.equal(index.get("charizard").base_attack, 223); + }); + + test("indexes on the normalized name", () => { + const index = indexBaseStats([{ name: "Ho-Oh", atk: 239, def: 244, sta: 214 }]); + assert.ok(index.get("ho oh")); + }); + + // Form-qualified names have no entry of their own — the API stores them as + // name + form — so a combined key is what makes them resolvable at all. + test("registers form-qualified keys alongside the bare name", () => { + const index = indexBaseStats([ + { name: "Deoxys", atk: 414, def: 46, sta: 137, form: "Attack" }, + { name: "Deoxys", atk: 144, def: 330, sta: 137, form: "Defense" }, + { name: "Deoxys", atk: 345, def: 115, sta: 137, form: "Normal" } + ]); + assert.equal(index.get("deoxys attack").base_attack, 414); + assert.equal(index.get("deoxys defense").base_attack, 144); + assert.equal(index.get("deoxys").base_attack, 345, "bare name resolves to Normal"); + }); + + test("accepts either spelling of a regional form", () => { + const index = indexBaseStats([ + { name: "Exeggutor", atk: 233, def: 149, sta: 216, form: "Alola" }, + { name: "Exeggutor", atk: 233, def: 149, sta: 190, form: "Normal" } + ]); + assert.ok(index.get("exeggutor alola"), "API spelling"); + assert.ok(index.get("exeggutor alolan"), "the spelling humans write"); + assert.equal(index.get("exeggutor alolan").base_stamina, 216); + assert.equal(index.get("exeggutor").base_stamina, 190, "bare name stays Normal"); + }); + + test("matches parenthesised form names as written in schedules", () => { + const index = indexBaseStats([ + { name: "Giratina", atk: 187, def: 225, sta: 284, form: "Altered" }, + { name: "Giratina", atk: 225, def: 187, sta: 284, form: "Origin" } + ]); + assert.equal(index.get(normalizeName("Giratina (Origin)")).base_attack, 225); + assert.equal(index.get(normalizeName("Giratina (Altered)")).base_attack, 187); + }); +}); + +describe("pokeApiSlug", () => { + test("handles plain and hyphenated names", () => { + assert.equal(pokeApiSlug("Charizard"), "charizard"); + assert.equal(pokeApiSlug("Ho-Oh"), "ho-oh"); + assert.equal(pokeApiSlug("Mr. Mime"), "mr-mime"); + assert.equal(pokeApiSlug("Farfetch'd"), "farfetchd"); + }); + + // Regression: parenthesised names produced "giratina-(origin)", a 400. + test("strips parentheses from form-qualified names", () => { + assert.equal(pokeApiSlug("Giratina (Origin)"), "giratina-origin"); + assert.equal(pokeApiSlug("Deoxys Attack"), "deoxys-attack"); + }); + + test("translates regional spellings to PokéAPI's", () => { + assert.equal(pokeApiSlug("Marowak (Alolan)"), "marowak-alola"); + assert.equal(pokeApiSlug("Exeggutor (Alolan)"), "exeggutor-alola"); + assert.equal(pokeApiSlug("Articuno (Galarian)"), "articuno-galar"); + }); + + test("returns an empty string for an empty name", () => { + assert.equal(pokeApiSlug(""), ""); + }); +}); + +describe("calcCatchCP", () => { + test("computes both catch levels from base stats", () => { + assert.deepEqual( + calcCatchCP({ base_attack: 100, base_defense: 100, base_stamina: 100 }), + { hundo: 471, whundo: 590 } + ); + }); + + test("weather-boosted CP is always the higher of the two", () => { + const cp = calcCatchCP({ base_attack: 223, base_defense: 173, base_stamina: 186 }); + assert.ok(cp.whundo > cp.hundo); + }); +}); + +describe("misc helpers", () => { + test("normalizeName folds case, punctuation and spacing", () => { + assert.equal(normalizeName("Ho-Oh"), "ho oh"); + assert.equal(normalizeName(" HO OH "), "ho oh"); + assert.equal(normalizeName("Exeggutor (Alolan)"), "exeggutor alolan"); + assert.equal(normalizeName("Mr. Mime"), "mr mime"); + assert.equal(normalizeName("Mr_mime"), "mr mime"); + // Apostrophes vanish rather than splitting the word + assert.equal(normalizeName("Farfetch'd"), "farfetchd"); + assert.equal(normalizeName("Farfetchd"), "farfetchd"); + }); + + test("mapEventTypeFromName prefers the longest match", () => { + assert.equal(mapEventTypeFromName("Charmander Community Day Classic"), "Community Day Classic"); + assert.equal(mapEventTypeFromName("Charmander Community Day"), "Community Day"); + assert.equal(mapEventTypeFromName("Max Battle Day"), "Max Battles"); + assert.equal(mapEventTypeFromName("Bake Sale"), null); + }); + + test("extractAttackFromDetails reads gets/learns phrasing", () => { + assert.equal(extractAttackFromDetails("Charizard gets Blast Burn, plus bonuses"), "Blast Burn"); + assert.equal(extractAttackFromDetails("learns Frenzy Plant."), "Frenzy Plant"); + assert.equal(extractAttackFromDetails("no attack here"), ""); + }); + + test("isShinyInDetails is a plain keyword check", () => { + assert.equal(isShinyInDetails("Boosted shiny odds"), true); + assert.equal(isShinyInDetails("nothing special"), false); + }); +}); + +describe("parseEventRow", () => { + test("builds a complete row from a schedule entry", () => { + const row = parseEventRow({ name: "Ho-Oh Raid Hour", details: "Boosted shiny raids", date: "August 12" }, NOW); + assert.equal(row.ok, true); + assert.equal(row.eventType, "Raid Hour"); + assert.equal(row.pokemon, "Ho-Oh"); + assert.equal(row.formattedDate, "August 12th"); + assert.equal(row.needsCP, true, "Raid Hour shows a CP line"); + }); + + test("flags rows whose event type cannot be identified", () => { + const row = parseEventRow({ name: "Mystery Gathering", details: "unclear", date: "August 20" }, NOW); + assert.equal(row.ok, false); + assert.match(row.reason, /event type/); + }); + + test("flags rows whose date cannot be parsed", () => { + assert.equal(parseEventRow({ name: "Charizard Raid Day", details: "x", date: "whenever" }, NOW).ok, false); + }); + + test("a day already gone in the current month is rejected", () => { + const midAugust = new Date("2026-08-20T12:00:00"); + assert.equal(parseEventRow({ name: "Charizard Raid Day", details: "x", date: "August 5" }, midAugust).ok, false); + }); + + test("an earlier month is read as next year, not as the past", () => { + // Pasting a January schedule in August means next January. + const row = parseEventRow({ name: "Charizard Raid Day", details: "x", date: "January 10" }, NOW); + assert.equal(row.ok, true); + assert.equal(row.formattedDate, "January 10th"); + }); + + test("asks for CP only when the event type shows a CP line", () => { + const raid = parseEventRow({ name: "Fakemon Raid Day", details: "x", date: "August 20" }, NOW); + assert.equal(raid.needsCP, true); + + const spotlight = parseEventRow({ name: "Fakemon Spotlight Hour", details: "x", date: "August 20" }, NOW); + assert.equal(spotlight.needsCP, false); + }); + + test("only collects a featured attack for types that use one", () => { + const cd = parseEventRow({ name: "Charmander Community Day", details: "Charmander gets Blast Burn", date: "August 16" }, NOW); + assert.equal(cd.attack, "Blast Burn"); + + const sh = parseEventRow({ name: "Charmander Spotlight Hour", details: "Charmander gets Blast Burn", date: "August 16" }, NOW); + assert.equal(sh.attack, ""); + }); +});