Move detail: list the Pokémon that can learn the move
The "Learned by N Pokémon" fact was a dead number. Now the move page
renders that list as a clickable sprite grid (links to each Pokémon),
with a filter box once it's more than ~24 long.
PokéAPI's learned_by_pokemon is flat across all games, so for a specific
game it's narrowed to species that existed by that generation — a
"Crystal" view of Surf shows 69 Gen 1–2 Pokémon, not the full 231. Not
perfectly game-exact (a mon that only gained the move in a later gen can
slip through) but far better than guess-and-check. National ("all") shows
the unfiltered list.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
This commit is contained in:
parent
3193b167ac
commit
86e9bc60bc
@ -102,7 +102,9 @@ Working app with a type-themed UI:
|
|||||||
/ "TMs only" / "HMs only" for the selected game and sort by power /
|
/ "TMs only" / "HMs only" for the selected game and sort by power /
|
||||||
accuracy / recency — or, with a machine filter on, by TM/HM number
|
accuracy / recency — or, with a machine filter on, by TM/HM number
|
||||||
(numbers are baked into the snapshot). Items filter by category. Each
|
(numbers are baked into the snapshot). Items filter by category. Each
|
||||||
has its own detail page. Query, scroll, tab and filters persist.
|
has its own detail page — the move page lists every Pokémon that can
|
||||||
|
learn it (filterable, narrowed to species that existed by the selected
|
||||||
|
game's generation). Query, scroll, tab and filters persist.
|
||||||
- **Team** — a lineup of up to 6. Coverage leads with a "weak spots"
|
- **Team** — a lineup of up to 6. Coverage leads with a "weak spots"
|
||||||
summary (types that hit 2+ members, or that someone's weak to and nobody
|
summary (types that hit 2+ members, or that someone's weak to and nobody
|
||||||
resists), then a full matchup grid with a diverging resist◀│▶weak bar per
|
resists), then a full matchup grid with a diverging resist◀│▶weak bar per
|
||||||
|
|||||||
@ -2700,6 +2700,9 @@
|
|||||||
grid-template-columns: repeat(auto-fill, minmax(88px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(88px, 1fr));
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
.learners__filter {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
.ab-mon {
|
.ab-mon {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@ -1,12 +1,15 @@
|
|||||||
import { el, clear } from '../lib/dom.js';
|
import { el, clear } from '../lib/dom.js';
|
||||||
import { getMove } from '../data/api.js';
|
import { getMove } from '../data/api.js';
|
||||||
|
import { loadSnapshot } from '../data/snapshot.js';
|
||||||
import { prettify } from '../data/pokedex-resolver.js';
|
import { prettify } from '../data/pokedex-resolver.js';
|
||||||
import { settings } from '../store/settings.js';
|
import { settings } from '../store/settings.js';
|
||||||
import { machineNumber } from '../components/MovesList.js';
|
import { machineNumber } from '../components/MovesList.js';
|
||||||
|
import { Sprite } from '../components/Sprite.js';
|
||||||
import { TypeChip } from '../components/TypeChip.js';
|
import { TypeChip } from '../components/TypeChip.js';
|
||||||
import { typeHex } from '../lib/type-color.js';
|
import { typeHex } from '../lib/type-color.js';
|
||||||
|
|
||||||
const DMG = { physical: 'Physical', special: 'Special', status: 'Status' };
|
const DMG = { physical: 'Physical', special: 'Special', status: 'Status' };
|
||||||
|
const idFromUrl = (u) => Number(u.replace(/\/$/, '').split('/').pop());
|
||||||
|
|
||||||
const english = (entries, key) => {
|
const english = (entries, key) => {
|
||||||
const hit = (entries || []).find((e) => e.language.name === 'en');
|
const hit = (entries || []).find((e) => e.language.name === 'en');
|
||||||
@ -21,6 +24,8 @@ export async function MoveDetail(id) {
|
|||||||
const view = el('section', { class: 'view lookup' });
|
const view = el('section', { class: 'view lookup' });
|
||||||
view.append(el('div', { class: 'view--loading' }, 'Loading move…'));
|
view.append(el('div', { class: 'view--loading' }, 'Loading move…'));
|
||||||
|
|
||||||
|
const snap = await loadSnapshot();
|
||||||
|
|
||||||
let d;
|
let d;
|
||||||
try {
|
try {
|
||||||
d = await getMove(id);
|
d = await getMove(id);
|
||||||
@ -64,6 +69,17 @@ export async function MoveDetail(id) {
|
|||||||
.join(', ');
|
.join(', ');
|
||||||
if (statChanges) bits.push(statChanges);
|
if (statChanges) bits.push(statChanges);
|
||||||
|
|
||||||
|
// Pokémon that can learn this move. PokéAPI only gives a flat, all-games
|
||||||
|
// list — narrow it to species that existed by the selected game's
|
||||||
|
// generation so a "Crystal" view isn't full of Gen 9 Pokémon.
|
||||||
|
const vg = settings.get().versionGroup;
|
||||||
|
const gen = snap.versionGroupByKey.get(vg)?.generation ?? 9;
|
||||||
|
let learners = (d.learned_by_pokemon || [])
|
||||||
|
.map((p) => snap.speciesById.get(idFromUrl(p.url)))
|
||||||
|
.filter(Boolean);
|
||||||
|
if (vg !== 'all') learners = learners.filter((sp) => sp.generation <= gen);
|
||||||
|
learners.sort((a, b) => a.id - b.id);
|
||||||
|
|
||||||
const facts = el(
|
const facts = el(
|
||||||
'dl',
|
'dl',
|
||||||
{ class: 'pfacts' },
|
{ class: 'pfacts' },
|
||||||
@ -72,7 +88,7 @@ export async function MoveDetail(id) {
|
|||||||
fact('PP', d.pp ?? '—'),
|
fact('PP', d.pp ?? '—'),
|
||||||
fact('Priority', d.priority ?? 0),
|
fact('Priority', d.priority ?? 0),
|
||||||
fact('Introduced', prettify(d.generation.name)),
|
fact('Introduced', prettify(d.generation.name)),
|
||||||
fact('Learned by', `${d.learned_by_pokemon.length} Pokémon`),
|
fact('Learned by', `${learners.length} Pokémon`),
|
||||||
);
|
);
|
||||||
if (d.machines?.length) {
|
if (d.machines?.length) {
|
||||||
const vg = settings.get().versionGroup;
|
const vg = settings.get().versionGroup;
|
||||||
@ -88,6 +104,56 @@ export async function MoveDetail(id) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Learners grid (filterable) ----------------------------------
|
||||||
|
const style = settings.get().spriteStyle;
|
||||||
|
const LEARNER_CAP = 400;
|
||||||
|
const learnersGrid = el('div', { class: 'ab-mons' });
|
||||||
|
function paintLearners(q) {
|
||||||
|
const qq = (q || '').trim().toLowerCase().replace(/\s+/g, '-');
|
||||||
|
const shown = qq ? learners.filter((sp) => sp.name.includes(qq)) : learners;
|
||||||
|
clear(learnersGrid);
|
||||||
|
if (!shown.length) {
|
||||||
|
learnersGrid.append(el('p', { class: 'detail__muted' }, 'No matches.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const sp of shown.slice(0, LEARNER_CAP)) {
|
||||||
|
learnersGrid.append(
|
||||||
|
el(
|
||||||
|
'a',
|
||||||
|
{ class: 'ab-mon', href: `#/pokemon/${sp.id}` },
|
||||||
|
Sprite(sp.id, { style, alt: sp.name, size: 48 }),
|
||||||
|
el('span', {}, sp.name.replace(/-/g, ' ')),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (shown.length > LEARNER_CAP) {
|
||||||
|
learnersGrid.append(
|
||||||
|
el('p', { class: 'detail__muted' }, `+${shown.length - LEARNER_CAP} more — filter to narrow.`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
paintLearners('');
|
||||||
|
const learnerFilter = el('input', {
|
||||||
|
type: 'search',
|
||||||
|
class: 'search__input learners__filter',
|
||||||
|
placeholder: 'Filter Pokémon…',
|
||||||
|
oninput: (e) => paintLearners(e.target.value),
|
||||||
|
});
|
||||||
|
|
||||||
|
const learnersSection = learners.length
|
||||||
|
? el(
|
||||||
|
'section',
|
||||||
|
{ class: 'detail__section' },
|
||||||
|
el(
|
||||||
|
'h2',
|
||||||
|
{},
|
||||||
|
`Pokémon that can learn this${vg === 'all' ? '' : ` in ${prettify(vg)}`} (${learners.length})`,
|
||||||
|
),
|
||||||
|
learners.length > 24 ? learnerFilter : null,
|
||||||
|
learnersGrid,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
clear(view).append(
|
clear(view).append(
|
||||||
el('nav', { class: 'lookup__nav' }, el('a', { class: 'link', href: '#/search' }, '‹ Lookup')),
|
el('nav', { class: 'lookup__nav' }, el('a', { class: 'link', href: '#/search' }, '‹ Lookup')),
|
||||||
el(
|
el(
|
||||||
@ -109,6 +175,7 @@ export async function MoveDetail(id) {
|
|||||||
el('p', {}, effect),
|
el('p', {}, effect),
|
||||||
bits.length ? el('p', { class: 'movebody__bits' }, bits.join(' · ')) : null,
|
bits.length ? el('p', { class: 'movebody__bits' }, bits.join(' · ')) : null,
|
||||||
),
|
),
|
||||||
|
learnersSection,
|
||||||
);
|
);
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user