From c9807127d661b4de83a0a7f34016037969434c31 Mon Sep 17 00:00:00 2001 From: chris Date: Fri, 4 Sep 2026 12:14:36 -0400 Subject: [PATCH] Dex grid: ability, egg group, min BST and fully-evolved filters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the existing filter drawer (type/gen) with four more: - Ability — snapshot abilities already carry a reverse pokemon[] index, so this is a plain lookup, no new data needed. - Egg group — options derived from the species list's own eggGroups. - Min BST — a number field; hard filter, distinct from sorting by BST. - Fully evolved only — new species.evolvesFromId field (from pokemon-species' evolves_from_species, already fetched during the snapshot build, so no extra requests). "Fully evolved" is computed client-side as "no other species points here via evolvesFromId" — correctly handles branching lines (Eevee's evolutions all count as fully evolved; Eevee itself doesn't). All four persist via the ui store and count toward the filter badge, same as the existing filters. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu --- README.md | 5 +- scripts/build-snapshot.mjs | 5 ++ src/store/ui.js | 4 ++ src/styles/layout.css | 23 +++++++++ src/views/DexGrid.js | 100 ++++++++++++++++++++++++++++++++++++- 5 files changed, 135 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 10bcf1a..6870a72 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,10 @@ src/ Working app with a type-themed UI: - **Dex grid** — type-tinted cards (official artwork, spotlight, ghost number), - per-dex progress, name/number filter, caught/favorite filters. + per-dex progress, name/number filter, caught/favorite filters, plus a + filter drawer: type, generation, ability, egg group, minimum BST, and + "fully evolved only" (derived client-side from each species' evolves-from + link — no extra fetches). - **Forms** — Megas, Gigantamax, regional forms and alternate formes in the snapshot; a pill switcher on the detail page rebuilds types / stats / matchups / abilities / learnset / artwork for the chosen form. Purely diff --git a/scripts/build-snapshot.mjs b/scripts/build-snapshot.mjs index c2dff0a..a871784 100644 --- a/scripts/build-snapshot.mjs +++ b/scripts/build-snapshot.mjs @@ -312,6 +312,11 @@ async function main() { color: sp.color?.name || null, eggGroups: sp.egg_groups.map((g) => g.name), genderRate: sp.gender_rate, + // Species this evolves FROM (null for base forms) — lets the app + // derive "fully evolved" client-side without fetching every + // evolution chain: a species nothing points to via this field has no + // further evolution. + evolvesFromId: sp.evolves_from_species ? idFromUrl(sp.evolves_from_species.url) : null, }; }); diff --git a/src/store/ui.js b/src/store/ui.js index 2010657..c536b3b 100644 --- a/src/store/ui.js +++ b/src/store/ui.js @@ -15,6 +15,10 @@ export const ui = createStore('pdx.ui', { filter: 'all', filterType: '', filterGen: 0, + filterAbility: 0, + filterEggGroup: '', + filterMinBst: 0, + filterFullyEvolved: false, recent: [], toolsOpen: false, teamMode: 'coverage', diff --git a/src/styles/layout.css b/src/styles/layout.css index a25c4ce..8e70ea3 100644 --- a/src/styles/layout.css +++ b/src/styles/layout.css @@ -1837,6 +1837,29 @@ max-width: none; flex: 1; } +.feed-minbst { + display: flex; + align-items: center; + gap: 6px; + flex: 1; + padding: 7px 12px; + border: 1.5px solid var(--border); + border-radius: 999px; + font-size: 0.84rem; + font-weight: 600; + color: var(--text-dim); + white-space: nowrap; +} +.feed-minbst input { + width: 4.5em; + min-width: 0; + border: none; + background: transparent; + color: var(--text); + font: inherit; + font-weight: 700; + padding: 0; +} .recent { display: flex; align-items: center; diff --git a/src/views/DexGrid.js b/src/views/DexGrid.js index a189873..3f877e5 100644 --- a/src/views/DexGrid.js +++ b/src/views/DexGrid.js @@ -82,6 +82,10 @@ export async function DexGrid() { let filterKey = ui.get().filter || 'all'; let filterType = ui.get().filterType || ''; let filterGen = Number(ui.get().filterGen) || 0; + let filterAbility = Number(ui.get().filterAbility) || 0; + let filterEggGroup = ui.get().filterEggGroup || ''; + let filterMinBst = Number(ui.get().filterMinBst) || 0; + let filterFullyEvolved = !!ui.get().filterFullyEvolved; let sortKey = ui.get().sort || 'dex'; let sortDesc = !!ui.get().sortDesc; let rows = []; @@ -210,6 +214,87 @@ export async function DexGrid() { '🎲', ); + // "Fully evolved" = no other species lists this one as evolvesFromId — + // derived client-side rather than fetching every evolution chain. + const hasNextEvo = new Set(snap.species.map((s) => s.evolvesFromId).filter(Boolean)); + const isFullyEvolved = (sp) => !hasNextEvo.has(sp.id); + + const eggGroupList = [...new Set(snap.species.flatMap((s) => s.eggGroups || []))].sort(); + const abilityList = snap.abilities.slice().sort((a, b) => a.name.localeCompare(b.name)); + + const abilitySelect = el( + 'select', + { + class: 'feed-sort__select', + onchange: (e) => { + filterAbility = Number(e.target.value) || 0; + ui.set({ filterAbility }); + paintGrid(); + resetScroll(); + syncToolsBadge(); + }, + }, + el('option', { value: '0' }, 'Any ability'), + ...abilityList.map((a) => + el('option', { value: String(a.id), selected: a.id === filterAbility }, prettify(a.name)), + ), + ); + + const eggGroupSelect = el( + 'select', + { + class: 'feed-sort__select', + onchange: (e) => { + filterEggGroup = e.target.value; + ui.set({ filterEggGroup }); + paintGrid(); + resetScroll(); + syncToolsBadge(); + }, + }, + el('option', { value: '' }, 'Any egg group'), + ...eggGroupList.map((g) => el('option', { value: g, selected: g === filterEggGroup }, prettify(g))), + ); + + const minBstField = el( + 'label', + { class: 'feed-minbst' }, + el('span', {}, 'Min BST'), + el('input', { + type: 'number', + min: '0', + max: '1000', + step: '10', + placeholder: '0', + value: filterMinBst || '', + onchange: (e) => { + filterMinBst = Math.max(0, Number(e.target.value) || 0); + e.target.value = filterMinBst || ''; + ui.set({ filterMinBst }); + paintGrid(); + resetScroll(); + syncToolsBadge(); + }, + }), + ); + + const fullyEvolvedCheck = el( + 'label', + { class: 'lookup__check' }, + el('input', { + type: 'checkbox', + checked: filterFullyEvolved, + onchange: (e) => { + filterFullyEvolved = e.target.checked; + ui.set({ filterFullyEvolved }); + paintGrid(); + resetScroll(); + syncToolsBadge(); + }, + }), + 'Fully evolved only', + ); + // Changing the list order/contents makes the old scroll offset meaningless. function resetScroll() { ui.set({ feedScroll: 0 }); @@ -263,6 +348,8 @@ export async function DexGrid() { filterBar, el('div', { class: 'feed-sort' }, el('span', { class: 'feed-sort__label' }, 'Sort'), sortSelect, dirBtn), el('div', { class: 'feed-sort feed-filters' }, typeSelect, genSelect, randomBtn), + el('div', { class: 'feed-sort feed-filters' }, abilitySelect, eggGroupSelect, minBstField), + el('div', { class: 'feed-sort feed-filters' }, fullyEvolvedCheck), ); const toolsToggle = el( 'button', @@ -281,7 +368,13 @@ export async function DexGrid() { ); function syncToolsBadge() { const n = - (filterKey !== 'all' ? 1 : 0) + (filterType ? 1 : 0) + (filterGen ? 1 : 0); + (filterKey !== 'all' ? 1 : 0) + + (filterType ? 1 : 0) + + (filterGen ? 1 : 0) + + (filterAbility ? 1 : 0) + + (filterEggGroup ? 1 : 0) + + (filterMinBst ? 1 : 0) + + (filterFullyEvolved ? 1 : 0); toolsToggle.dataset.active = n ? String(n) : ''; } syncToolsBadge(); @@ -337,6 +430,7 @@ export async function DexGrid() { const gen = snap.versionGroupByKey.get(st.versionGroup)?.generation ?? 9; const boxed = st.spriteStyle === 'game' && gen <= 2; const filterTest = FILTERS.find((f) => f.key === filterKey).test; + const abilityMon = filterAbility ? new Set(snap.abilityById.get(filterAbility)?.pokemon || []) : null; let list = rows.filter(({ species, number }) => { if ( @@ -349,6 +443,10 @@ export async function DexGrid() { } if (filterGen && species.generation !== filterGen) return false; if (filterType && !typesForGen(species, gen).includes(filterType)) return false; + if (abilityMon && !abilityMon.has(species.id)) return false; + if (filterEggGroup && !(species.eggGroups || []).includes(filterEggGroup)) return false; + if (filterMinBst && (species.bst || 0) < filterMinBst) return false; + if (filterFullyEvolved && !isFullyEvolved(species)) return false; const e = pokemonState[species.id] || { seen: false, caught: false, favorite: false }; return filterTest(e, species); });