Abilities lookup + detail; Natures reference
- Snapshot bakes an abilities index (373: name, generation, effect, and the species that have it). Snapshot ~872 KB / 133 KB gzip. - Search gains an 'Abilities' tab — browse all 373 offline, filter/search by name or effect, sort A–Z / most-Pokemon / newest - #/ability/:id detail: effect + a grid of every Pokemon with the ability (all from the snapshot, no fetch) - Pokemon detail: the Abilities line is now tappable, linking to each ability - #/natures: the full 25-nature +10%/-10% table; linked from the Team view Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
79a5efaf11
commit
415cf3edec
@ -220,6 +220,31 @@ async function main() {
|
||||
});
|
||||
moves.sort((a, b) => a.id - b.id);
|
||||
|
||||
// ---- Abilities index ------------------------------------------
|
||||
const abilityIndex = await api('ability?limit=100000');
|
||||
console.log(`Fetching ${abilityIndex.results.length} abilities …`);
|
||||
const abilities = await mapLimit(abilityIndex.results, CONCURRENCY, async (r) => {
|
||||
const a = await api(r.url);
|
||||
const en = (a.effect_entries || []).find((e) => e.language.name === 'en');
|
||||
const flavour = [...(a.flavor_text_entries || [])]
|
||||
.reverse()
|
||||
.find((e) => e.language.name === 'en');
|
||||
return {
|
||||
id: a.id,
|
||||
name: a.name,
|
||||
generation: idFromUrl(a.generation.url),
|
||||
isMainSeries: a.is_main_series,
|
||||
effect:
|
||||
(en && (en.short_effect || en.effect)) ||
|
||||
(flavour && flavour.flavor_text) ||
|
||||
'',
|
||||
pokemon: [
|
||||
...new Set(a.pokemon.map((x) => idFromUrl(x.pokemon.url)).filter((n) => n <= 100000)),
|
||||
],
|
||||
};
|
||||
});
|
||||
abilities.sort((a, b) => a.id - b.id);
|
||||
|
||||
// ---- Item categories (gives every item a category cheaply) ----
|
||||
const catIndex = await api('item-category?limit=100');
|
||||
const items = [];
|
||||
@ -240,6 +265,7 @@ async function main() {
|
||||
species: species.length,
|
||||
moves: moves.length,
|
||||
items: items.length,
|
||||
abilities: abilities.length,
|
||||
pokedexes: pokedexes.length,
|
||||
versionGroups: versionGroups.length,
|
||||
},
|
||||
@ -249,6 +275,7 @@ async function main() {
|
||||
species,
|
||||
moves,
|
||||
items,
|
||||
abilities,
|
||||
pokedexes,
|
||||
versionGroups,
|
||||
};
|
||||
|
||||
39
src/data/natures.js
Normal file
39
src/data/natures.js
Normal file
@ -0,0 +1,39 @@
|
||||
/**
|
||||
* The 25 natures. Each raises one stat by 10% and lowers another by 10%;
|
||||
* the five where up === down are neutral. HP is never affected.
|
||||
*/
|
||||
export const STAT_LABEL = {
|
||||
atk: 'Attack',
|
||||
def: 'Defense',
|
||||
spa: 'Sp. Atk',
|
||||
spd: 'Sp. Def',
|
||||
spe: 'Speed',
|
||||
};
|
||||
|
||||
export const NATURES = [
|
||||
{ name: 'Hardy', up: 'atk', down: 'atk' },
|
||||
{ name: 'Lonely', up: 'atk', down: 'def' },
|
||||
{ name: 'Brave', up: 'atk', down: 'spe' },
|
||||
{ name: 'Adamant', up: 'atk', down: 'spa' },
|
||||
{ name: 'Naughty', up: 'atk', down: 'spd' },
|
||||
{ name: 'Bold', up: 'def', down: 'atk' },
|
||||
{ name: 'Docile', up: 'def', down: 'def' },
|
||||
{ name: 'Relaxed', up: 'def', down: 'spe' },
|
||||
{ name: 'Impish', up: 'def', down: 'spa' },
|
||||
{ name: 'Lax', up: 'def', down: 'spd' },
|
||||
{ name: 'Timid', up: 'spe', down: 'atk' },
|
||||
{ name: 'Hasty', up: 'spe', down: 'def' },
|
||||
{ name: 'Serious', up: 'spe', down: 'spe' },
|
||||
{ name: 'Jolly', up: 'spe', down: 'spa' },
|
||||
{ name: 'Naive', up: 'spe', down: 'spd' },
|
||||
{ name: 'Modest', up: 'spa', down: 'atk' },
|
||||
{ name: 'Mild', up: 'spa', down: 'def' },
|
||||
{ name: 'Quiet', up: 'spa', down: 'spe' },
|
||||
{ name: 'Bashful', up: 'spa', down: 'spa' },
|
||||
{ name: 'Rash', up: 'spa', down: 'spd' },
|
||||
{ name: 'Calm', up: 'spd', down: 'atk' },
|
||||
{ name: 'Gentle', up: 'spd', down: 'def' },
|
||||
{ name: 'Sassy', up: 'spd', down: 'spe' },
|
||||
{ name: 'Careful', up: 'spd', down: 'spa' },
|
||||
{ name: 'Quirky', up: 'spd', down: 'spd' },
|
||||
];
|
||||
@ -22,8 +22,10 @@ export async function loadSnapshot() {
|
||||
data.generationById = new Map(data.generations.map((g) => [g.id, g]));
|
||||
data.moves = data.moves || [];
|
||||
data.items = data.items || [];
|
||||
data.abilities = data.abilities || [];
|
||||
data.moveById = new Map(data.moves.map((m) => [m.id, m]));
|
||||
data.itemById = new Map(data.items.map((it) => [it.id, it]));
|
||||
data.abilityById = new Map(data.abilities.map((a) => [a.id, a]));
|
||||
data.itemCategories = [...new Set(data.items.map((it) => it.category))].sort();
|
||||
|
||||
cached = data;
|
||||
|
||||
@ -5,7 +5,9 @@ import { SearchView } from './views/SearchView.js';
|
||||
import { SettingsView } from './views/SettingsView.js';
|
||||
import { MoveDetail } from './views/MoveDetail.js';
|
||||
import { ItemDetail } from './views/ItemDetail.js';
|
||||
import { AbilityDetail } from './views/AbilityDetail.js';
|
||||
import { TeamView } from './views/TeamView.js';
|
||||
import { NaturesView } from './views/NaturesView.js';
|
||||
import { detailSkeleton, lookupSkeleton } from './components/skeletons.js';
|
||||
|
||||
const routes = [
|
||||
@ -13,7 +15,9 @@ const routes = [
|
||||
{ pattern: /^#\/pokemon\/(\d+)$/, view: (m) => PokemonDetail(Number(m[1])), skeleton: detailSkeleton },
|
||||
{ pattern: /^#\/move\/(\d+)$/, view: (m) => MoveDetail(Number(m[1])), skeleton: lookupSkeleton },
|
||||
{ pattern: /^#\/item\/(\d+)$/, view: (m) => ItemDetail(Number(m[1])), skeleton: lookupSkeleton },
|
||||
{ pattern: /^#\/ability\/(\d+)$/, view: (m) => AbilityDetail(Number(m[1])), skeleton: lookupSkeleton },
|
||||
{ pattern: /^#\/team$/, view: () => TeamView() },
|
||||
{ pattern: /^#\/natures$/, view: () => NaturesView() },
|
||||
{ pattern: /^#\/search$/, view: () => SearchView() },
|
||||
{ pattern: /^#\/settings$/, view: () => SettingsView() },
|
||||
];
|
||||
|
||||
@ -24,4 +24,5 @@ export const ui = createStore('pdx.ui', {
|
||||
mvTm: false,
|
||||
itCat: '',
|
||||
itSort: 'name',
|
||||
abSort: 'name',
|
||||
});
|
||||
|
||||
@ -2305,3 +2305,88 @@
|
||||
border-radius: 6px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
/* ---- Abilities ---------------------------------------------- */
|
||||
.search__row--ability {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px 10px;
|
||||
padding: 11px 14px;
|
||||
}
|
||||
.search__ab-eff {
|
||||
flex-basis: 100%;
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-dim);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ab-mons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(88px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
.ab-mon {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 8px 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--surface-2);
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
text-transform: capitalize;
|
||||
text-align: center;
|
||||
}
|
||||
.ab-mon:hover {
|
||||
background: color-mix(in srgb, var(--accent) 18%, var(--surface-2));
|
||||
}
|
||||
.ab-mon .sprite {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* ---- Natures table --------------------------------------- */
|
||||
.natures {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.natures__row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 8px;
|
||||
padding: 9px 6px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
.natures__row--head {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.natures__name {
|
||||
font-weight: 700;
|
||||
}
|
||||
.natures__up {
|
||||
color: var(--good);
|
||||
font-weight: 600;
|
||||
}
|
||||
.natures__down {
|
||||
color: var(--danger);
|
||||
font-weight: 600;
|
||||
}
|
||||
.natures__row.is-neutral {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.natures__row.is-neutral .natures__up,
|
||||
.natures__row.is-neutral .natures__down {
|
||||
color: var(--text-dim);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
66
src/views/AbilityDetail.js
Normal file
66
src/views/AbilityDetail.js
Normal file
@ -0,0 +1,66 @@
|
||||
import { el, clear } from '../lib/dom.js';
|
||||
import { loadSnapshot } from '../data/snapshot.js';
|
||||
import { settings } from '../store/settings.js';
|
||||
import { Sprite } from '../components/Sprite.js';
|
||||
|
||||
export async function AbilityDetail(id) {
|
||||
const view = el('section', { class: 'view lookup' });
|
||||
const snap = await loadSnapshot();
|
||||
const a = snap.abilityById.get(id);
|
||||
|
||||
if (!a) {
|
||||
view.append(
|
||||
el(
|
||||
'div',
|
||||
{ class: 'view--error' },
|
||||
el('h1', {}, 'Unknown ability'),
|
||||
el('a', { class: 'button', href: '#/search' }, 'Back to lookup'),
|
||||
),
|
||||
);
|
||||
return view;
|
||||
}
|
||||
|
||||
const style = settings.get().spriteStyle;
|
||||
const mons = a.pokemon.map((pid) => snap.speciesById.get(pid)).filter(Boolean);
|
||||
mons.sort((x, y) => x.id - y.id);
|
||||
|
||||
clear(view).append(
|
||||
el('nav', { class: 'lookup__nav' }, el('a', { class: 'link', href: '#/search' }, '‹ Lookup')),
|
||||
el(
|
||||
'header',
|
||||
{ class: 'lookup__head lookup__head--item' },
|
||||
el(
|
||||
'div',
|
||||
{},
|
||||
el('h1', {}, a.name.replace(/-/g, ' ')),
|
||||
el('span', { class: 'lookup__class' }, `Introduced Gen ${a.generation}`),
|
||||
),
|
||||
),
|
||||
el(
|
||||
'section',
|
||||
{ class: 'detail__section' },
|
||||
el('h2', {}, 'Effect'),
|
||||
el('p', {}, a.effect || 'No description available.'),
|
||||
),
|
||||
el(
|
||||
'section',
|
||||
{ class: 'detail__section' },
|
||||
el('h2', {}, `Pokémon with this ability (${mons.length})`),
|
||||
mons.length
|
||||
? el(
|
||||
'div',
|
||||
{ class: 'ab-mons' },
|
||||
...mons.map((sp) =>
|
||||
el(
|
||||
'a',
|
||||
{ class: 'ab-mon', href: `#/pokemon/${sp.id}` },
|
||||
Sprite(sp.id, { style, alt: sp.name, size: 48 }),
|
||||
el('span', {}, sp.name.replace(/-/g, ' ')),
|
||||
),
|
||||
),
|
||||
)
|
||||
: el('p', { class: 'detail__muted' }, 'None on record.'),
|
||||
),
|
||||
);
|
||||
return view;
|
||||
}
|
||||
36
src/views/NaturesView.js
Normal file
36
src/views/NaturesView.js
Normal file
@ -0,0 +1,36 @@
|
||||
import { el } from '../lib/dom.js';
|
||||
import { NATURES, STAT_LABEL } from '../data/natures.js';
|
||||
|
||||
export function NaturesView() {
|
||||
const view = el('section', { class: 'view lookup' });
|
||||
view.append(
|
||||
el(
|
||||
'header',
|
||||
{ class: 'view__header' },
|
||||
el('h1', {}, 'Natures'),
|
||||
el('p', {}, 'Each nature raises one stat by 10% and lowers another by 10%. HP is never affected; five natures are neutral.'),
|
||||
),
|
||||
el(
|
||||
'div',
|
||||
{ class: 'natures' },
|
||||
el(
|
||||
'div',
|
||||
{ class: 'natures__row natures__row--head' },
|
||||
el('span', {}, 'Nature'),
|
||||
el('span', { class: 'natures__up' }, '▲ raises'),
|
||||
el('span', { class: 'natures__down' }, '▼ lowers'),
|
||||
),
|
||||
...NATURES.map((n) => {
|
||||
const neutral = n.up === n.down;
|
||||
return el(
|
||||
'div',
|
||||
{ class: `natures__row${neutral ? ' is-neutral' : ''}` },
|
||||
el('span', { class: 'natures__name' }, n.name),
|
||||
el('span', { class: 'natures__up' }, neutral ? '—' : STAT_LABEL[n.up]),
|
||||
el('span', { class: 'natures__down' }, neutral ? '—' : STAT_LABEL[n.down]),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
return view;
|
||||
}
|
||||
@ -242,9 +242,19 @@ export async function PokemonDetail(nationalId) {
|
||||
syncTrack();
|
||||
|
||||
// ---- About panel -----------------------------------------------
|
||||
const abilities = pokemon.abilities
|
||||
.filter((a) => !a.is_hidden || vgGen >= 5)
|
||||
.map((a) => prettify(a.ability.name) + (a.is_hidden ? ' (hidden)' : ''));
|
||||
const abilityList = pokemon.abilities.filter((a) => !a.is_hidden || vgGen >= 5);
|
||||
const abilitiesNode = el(
|
||||
'span',
|
||||
{},
|
||||
...abilityList.flatMap((a, i) => {
|
||||
const link = el(
|
||||
'a',
|
||||
{ class: 'link', href: `#/ability/${idFromUrl(a.ability.url)}` },
|
||||
prettify(a.ability.name) + (a.is_hidden ? ' (hidden)' : ''),
|
||||
);
|
||||
return i === 0 ? [link] : [', ', link];
|
||||
}),
|
||||
);
|
||||
|
||||
const evYield =
|
||||
pokemon.stats
|
||||
@ -267,7 +277,7 @@ export async function PokemonDetail(nationalId) {
|
||||
{ class: 'pfacts' },
|
||||
fact('Height', `${(pokemon.height / 10).toFixed(1)} m`),
|
||||
fact('Weight', `${(pokemon.weight / 10).toFixed(1)} kg`),
|
||||
abilities.length && vgGen >= 3 ? fact('Abilities', abilities.join(', ')) : null,
|
||||
abilityList.length && vgGen >= 3 ? fact('Abilities', abilitiesNode) : null,
|
||||
fact('Introduced', prettify(species.generation.name)),
|
||||
species.genera?.length
|
||||
? fact(
|
||||
|
||||
@ -22,6 +22,7 @@ const TABS = [
|
||||
{ id: 'pokemon', label: 'Pokémon' },
|
||||
{ id: 'moves', label: 'Moves' },
|
||||
{ id: 'items', label: 'Items' },
|
||||
{ id: 'abilities', label: 'Abilities' },
|
||||
];
|
||||
|
||||
const tmLabel = (raw) => {
|
||||
@ -140,12 +141,30 @@ export async function SearchView() {
|
||||
run();
|
||||
}),
|
||||
);
|
||||
} else if (tab === 'abilities') {
|
||||
filters.append(
|
||||
pill(
|
||||
'abSort',
|
||||
[['name', 'A–Z'], ['count', 'Most Pokémon'], ['gen', 'Newest']],
|
||||
ui.get().abSort,
|
||||
(v) => {
|
||||
ui.set({ abSort: v });
|
||||
run();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function syncUI() {
|
||||
input.placeholder =
|
||||
tab === 'pokemon' ? 'Name, number or type…' : tab === 'moves' ? 'Move name…' : 'Item name…';
|
||||
tab === 'pokemon'
|
||||
? 'Name, number or type…'
|
||||
: tab === 'moves'
|
||||
? 'Move name…'
|
||||
: tab === 'items'
|
||||
? 'Item name…'
|
||||
: 'Ability name or effect…';
|
||||
buildFilters();
|
||||
}
|
||||
|
||||
@ -285,6 +304,32 @@ export async function SearchView() {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Abilities (from snapshot, offline) ---------------------
|
||||
function renderAbilities(q) {
|
||||
const sort = ui.get().abSort;
|
||||
let list = snap.abilities.filter(
|
||||
(a) => !q || loose(a.name).includes(q) || a.effect.toLowerCase().includes(q),
|
||||
);
|
||||
list.sort((a, b) => {
|
||||
if (sort === 'count') return b.pokemon.length - a.pokemon.length || a.name.localeCompare(b.name);
|
||||
if (sort === 'gen') return b.generation - a.generation || a.name.localeCompare(b.name);
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
note.textContent = `${list.length} abilit${list.length === 1 ? 'y' : 'ies'}`;
|
||||
for (const a of list.slice(0, 400)) {
|
||||
results.append(
|
||||
el(
|
||||
'a',
|
||||
{ class: 'search__row search__row--ability', href: `#/ability/${a.id}` },
|
||||
el('span', { class: 'search__name' }, a.name.replace(/-/g, ' ')),
|
||||
el('span', { class: 'search__cat' }, `Gen ${a.generation}`),
|
||||
el('span', { class: 'search__cat' }, `${a.pokemon.length} 🐾`),
|
||||
el('span', { class: 'search__ab-eff' }, a.effect || '—'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function run() {
|
||||
const token = ++runToken;
|
||||
const q = loose(query);
|
||||
@ -299,7 +344,8 @@ export async function SearchView() {
|
||||
return;
|
||||
}
|
||||
if (tab === 'moves') renderMoves(q, token);
|
||||
else renderItems(q);
|
||||
else if (tab === 'items') renderItems(q);
|
||||
else renderAbilities(q);
|
||||
}
|
||||
|
||||
run();
|
||||
|
||||
@ -60,7 +60,12 @@ export async function TeamView() {
|
||||
'header',
|
||||
{ class: 'view__header' },
|
||||
el('h1', {}, 'Team'),
|
||||
el('p', {}, `Up to ${MAX_TEAM} Pokémon — check weaknesses, coverage and stats.`),
|
||||
el(
|
||||
'p',
|
||||
{},
|
||||
`Up to ${MAX_TEAM} Pokémon — check weaknesses, coverage and stats. `,
|
||||
el('a', { class: 'link', href: '#/natures' }, 'Natures reference'),
|
||||
),
|
||||
),
|
||||
seg,
|
||||
lineup,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user