Dex grid: ability, egg group, min BST and fully-evolved filters
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
This commit is contained in:
parent
fecafe139c
commit
c9807127d6
@ -70,7 +70,10 @@ src/
|
|||||||
Working app with a type-themed UI:
|
Working app with a type-themed UI:
|
||||||
|
|
||||||
- **Dex grid** — type-tinted cards (official artwork, spotlight, ghost number),
|
- **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
|
- **Forms** — Megas, Gigantamax, regional forms and alternate formes in the
|
||||||
snapshot; a pill switcher on the detail page rebuilds types / stats /
|
snapshot; a pill switcher on the detail page rebuilds types / stats /
|
||||||
matchups / abilities / learnset / artwork for the chosen form. Purely
|
matchups / abilities / learnset / artwork for the chosen form. Purely
|
||||||
|
|||||||
@ -312,6 +312,11 @@ async function main() {
|
|||||||
color: sp.color?.name || null,
|
color: sp.color?.name || null,
|
||||||
eggGroups: sp.egg_groups.map((g) => g.name),
|
eggGroups: sp.egg_groups.map((g) => g.name),
|
||||||
genderRate: sp.gender_rate,
|
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,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,10 @@ export const ui = createStore('pdx.ui', {
|
|||||||
filter: 'all',
|
filter: 'all',
|
||||||
filterType: '',
|
filterType: '',
|
||||||
filterGen: 0,
|
filterGen: 0,
|
||||||
|
filterAbility: 0,
|
||||||
|
filterEggGroup: '',
|
||||||
|
filterMinBst: 0,
|
||||||
|
filterFullyEvolved: false,
|
||||||
recent: [],
|
recent: [],
|
||||||
toolsOpen: false,
|
toolsOpen: false,
|
||||||
teamMode: 'coverage',
|
teamMode: 'coverage',
|
||||||
|
|||||||
@ -1837,6 +1837,29 @@
|
|||||||
max-width: none;
|
max-width: none;
|
||||||
flex: 1;
|
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 {
|
.recent {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -82,6 +82,10 @@ export async function DexGrid() {
|
|||||||
let filterKey = ui.get().filter || 'all';
|
let filterKey = ui.get().filter || 'all';
|
||||||
let filterType = ui.get().filterType || '';
|
let filterType = ui.get().filterType || '';
|
||||||
let filterGen = Number(ui.get().filterGen) || 0;
|
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 sortKey = ui.get().sort || 'dex';
|
||||||
let sortDesc = !!ui.get().sortDesc;
|
let sortDesc = !!ui.get().sortDesc;
|
||||||
let rows = [];
|
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.
|
// Changing the list order/contents makes the old scroll offset meaningless.
|
||||||
function resetScroll() {
|
function resetScroll() {
|
||||||
ui.set({ feedScroll: 0 });
|
ui.set({ feedScroll: 0 });
|
||||||
@ -263,6 +348,8 @@ export async function DexGrid() {
|
|||||||
filterBar,
|
filterBar,
|
||||||
el('div', { class: 'feed-sort' }, el('span', { class: 'feed-sort__label' }, 'Sort'), sortSelect, dirBtn),
|
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' }, typeSelect, genSelect, randomBtn),
|
||||||
|
el('div', { class: 'feed-sort feed-filters' }, abilitySelect, eggGroupSelect, minBstField),
|
||||||
|
el('div', { class: 'feed-sort feed-filters' }, fullyEvolvedCheck),
|
||||||
);
|
);
|
||||||
const toolsToggle = el(
|
const toolsToggle = el(
|
||||||
'button',
|
'button',
|
||||||
@ -281,7 +368,13 @@ export async function DexGrid() {
|
|||||||
);
|
);
|
||||||
function syncToolsBadge() {
|
function syncToolsBadge() {
|
||||||
const n =
|
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) : '';
|
toolsToggle.dataset.active = n ? String(n) : '';
|
||||||
}
|
}
|
||||||
syncToolsBadge();
|
syncToolsBadge();
|
||||||
@ -337,6 +430,7 @@ export async function DexGrid() {
|
|||||||
const gen = snap.versionGroupByKey.get(st.versionGroup)?.generation ?? 9;
|
const gen = snap.versionGroupByKey.get(st.versionGroup)?.generation ?? 9;
|
||||||
const boxed = st.spriteStyle === 'game' && gen <= 2;
|
const boxed = st.spriteStyle === 'game' && gen <= 2;
|
||||||
const filterTest = FILTERS.find((f) => f.key === filterKey).test;
|
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 }) => {
|
let list = rows.filter(({ species, number }) => {
|
||||||
if (
|
if (
|
||||||
@ -349,6 +443,10 @@ export async function DexGrid() {
|
|||||||
}
|
}
|
||||||
if (filterGen && species.generation !== filterGen) return false;
|
if (filterGen && species.generation !== filterGen) return false;
|
||||||
if (filterType && !typesForGen(species, gen).includes(filterType)) 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 };
|
const e = pokemonState[species.id] || { seen: false, caught: false, favorite: false };
|
||||||
return filterTest(e, species);
|
return filterTest(e, species);
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user