Dense-data pass: training/breeding facts, STAB coverage, filters, cries
- Detail About tab: EV yield, base EXP, catch rate + ~%, base friendship, growth rate + total EXP, held items; Breeding block with a gender-ratio bar, egg groups, egg cycles/steps - Stats tab: 'STAB coverage (attacking)' — types the Pokemon's STAB hits hard / is walled by (data/type-chart.js offensiveSummary) - Feed: Type and Generation filter dropdowns + a Random button; cards now resolve typings for the selected game's generation via snapshot pastTypes - Recently-viewed strip on the feed (ui.recent, capped at 12) - Play-cry button on the detail hero; service worker caches PokeAPI cries Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
8ebdb9d197
commit
2e4572428a
@ -3,15 +3,17 @@ import { Sprite } from './Sprite.js';
|
|||||||
import { TypeChip } from './TypeChip.js';
|
import { TypeChip } from './TypeChip.js';
|
||||||
import { entry, toggle } from '../store/selection.js';
|
import { entry, toggle } from '../store/selection.js';
|
||||||
import { typeHex } from '../lib/type-color.js';
|
import { typeHex } from '../lib/type-color.js';
|
||||||
|
import { typesForGen } from '../lib/type-resolve.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One Pokémon in the dex feed. Tinted by its primary type: a soft top-down
|
* One Pokémon in the dex feed. Tinted by its primary type: a soft top-down
|
||||||
* gradient, a type-coloured spotlight behind an oversized sprite that lifts
|
* gradient, a type-coloured spotlight behind an oversized sprite that lifts
|
||||||
* above the card, a number chip, and a big ghost number in the corner.
|
* above the card, a number chip, and a big ghost number in the corner.
|
||||||
*/
|
*/
|
||||||
export function Card(species, number, { spriteStyle = 'official', versionGroup, boxed = false, metric = null } = {}) {
|
export function Card(species, number, { spriteStyle = 'official', versionGroup, gen = 9, boxed = false, metric = null } = {}) {
|
||||||
const state = entry(species.id);
|
const state = entry(species.id);
|
||||||
const mainType = (species.types || [])[0] || 'normal';
|
const types = typesForGen(species, gen);
|
||||||
|
const mainType = types[0] || 'normal';
|
||||||
const num = String(number ?? species.id).padStart(3, '0');
|
const num = String(number ?? species.id).padStart(3, '0');
|
||||||
const data = { type: mainType, sprite: spriteStyle };
|
const data = { type: mainType, sprite: spriteStyle };
|
||||||
if (boxed) data.boxed = '1';
|
if (boxed) data.boxed = '1';
|
||||||
@ -58,7 +60,7 @@ export function Card(species, number, { spriteStyle = 'official', versionGroup,
|
|||||||
{ class: 'card__body' },
|
{ class: 'card__body' },
|
||||||
el('span', { class: 'card__num' }, `#${num}`),
|
el('span', { class: 'card__num' }, `#${num}`),
|
||||||
el('span', { class: 'card__name' }, species.name.replace(/-/g, ' ')),
|
el('span', { class: 'card__name' }, species.name.replace(/-/g, ' ')),
|
||||||
el('span', { class: 'card__types' }, ...(species.types || []).map(TypeChip)),
|
el('span', { class: 'card__types' }, ...types.map(TypeChip)),
|
||||||
metric ? el('span', { class: 'card__metric' }, metric) : null,
|
metric ? el('span', { class: 'card__metric' }, metric) : null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -64,6 +64,23 @@ export function multiplier(attacking, defTypes, gen = 9) {
|
|||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What a Pokémon's STAB types hit, from the attacker's side.
|
||||||
|
* Returns { strong: [types any STAB is 2x+ against], walls: [types that
|
||||||
|
* resist every one of its STAB types] }.
|
||||||
|
*/
|
||||||
|
export function offensiveSummary(atkTypes, gen = 9) {
|
||||||
|
const strong = [];
|
||||||
|
const walls = [];
|
||||||
|
const defs = gen < 6 ? TYPES.filter((t) => t !== 'fairy') : TYPES;
|
||||||
|
for (const def of defs) {
|
||||||
|
const mults = atkTypes.map((atk) => multiplier(atk, [def], gen));
|
||||||
|
if (Math.max(...mults) >= 2) strong.push(def);
|
||||||
|
else if (mults.every((m) => m <= 0.5)) walls.push(def);
|
||||||
|
}
|
||||||
|
return { strong, walls };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defensive matchups for a Pokémon, bucketed by multiplier.
|
* Defensive matchups for a Pokémon, bucketed by multiplier.
|
||||||
* Returns { '4': [...types], '2': [...], '0.5': [...], '0.25': [...], '0': [...] }
|
* Returns { '4': [...types], '2': [...], '0.5': [...], '0.25': [...], '0': [...] }
|
||||||
|
|||||||
13
src/lib/type-resolve.js
Normal file
13
src/lib/type-resolve.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* A snapshot species' typing as it was in a given generation, using the
|
||||||
|
* `pastTypes` history baked into the snapshot (Gen 1 Magnemite is pure
|
||||||
|
* Electric, pre-Gen 6 Clefairy is Normal, …). Falls back to current types.
|
||||||
|
*/
|
||||||
|
export function typesForGen(species, gen = 9) {
|
||||||
|
const past = species && species.pastTypes;
|
||||||
|
if (past && past.length) {
|
||||||
|
const era = [...past].sort((a, b) => a.gen - b.gen).find((p) => p.gen >= gen);
|
||||||
|
if (era) return era.types;
|
||||||
|
}
|
||||||
|
return (species && species.types) || [];
|
||||||
|
}
|
||||||
@ -13,4 +13,7 @@ export const ui = createStore('pdx.ui', {
|
|||||||
sort: 'dex',
|
sort: 'dex',
|
||||||
sortDesc: false,
|
sortDesc: false,
|
||||||
filter: 'all',
|
filter: 'all',
|
||||||
|
filterType: '',
|
||||||
|
filterGen: 0,
|
||||||
|
recent: [],
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1825,3 +1825,77 @@
|
|||||||
animation: none;
|
animation: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Feed: type/gen filters, recent strip -------------------- */
|
||||||
|
.feed-filters {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.feed-filters .feed-sort__select {
|
||||||
|
max-width: none;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.recent {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
}
|
||||||
|
.recent::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.recent[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.recent__label {
|
||||||
|
flex: none;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
.recent__item {
|
||||||
|
flex: none;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--surface-2);
|
||||||
|
transition: transform 0.12s var(--ease-spring);
|
||||||
|
}
|
||||||
|
.recent__item:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
background: color-mix(in srgb, var(--accent) 20%, var(--surface-2));
|
||||||
|
}
|
||||||
|
.recent__item .sprite {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Detail: gender bar, cry button ------------------------- */
|
||||||
|
.gender {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
.gender-bar {
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
#4d90d5 calc(100% - var(--f, 0%)),
|
||||||
|
#ec8fe6 calc(100% - var(--f, 0%))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
.gender span {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
.phero__cry {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 6px 11px;
|
||||||
|
}
|
||||||
|
|||||||
@ -34,7 +34,7 @@ registerRoute(
|
|||||||
registerRoute(
|
registerRoute(
|
||||||
({ url }) =>
|
({ url }) =>
|
||||||
url.origin === 'https://raw.githubusercontent.com' &&
|
url.origin === 'https://raw.githubusercontent.com' &&
|
||||||
url.pathname.includes('/PokeAPI/sprites/'),
|
(url.pathname.includes('/PokeAPI/sprites/') || url.pathname.includes('/PokeAPI/cries/')),
|
||||||
new CacheFirst({
|
new CacheFirst({
|
||||||
cacheName: 'pokeapi-img',
|
cacheName: 'pokeapi-img',
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|||||||
@ -6,6 +6,13 @@ import { ui } from '../store/ui.js';
|
|||||||
import { selection, stats } from '../store/selection.js';
|
import { selection, stats } from '../store/selection.js';
|
||||||
import { Card } from '../components/Card.js';
|
import { Card } from '../components/Card.js';
|
||||||
import { ProgressRing } from '../components/ProgressRing.js';
|
import { ProgressRing } from '../components/ProgressRing.js';
|
||||||
|
import { Sprite } from '../components/Sprite.js';
|
||||||
|
import { typesForGen } from '../lib/type-resolve.js';
|
||||||
|
|
||||||
|
const TYPES = [
|
||||||
|
'normal', 'fire', 'water', 'electric', 'grass', 'ice', 'fighting', 'poison',
|
||||||
|
'ground', 'flying', 'psychic', 'bug', 'rock', 'ghost', 'dragon', 'dark', 'steel', 'fairy',
|
||||||
|
];
|
||||||
|
|
||||||
const FILTERS = [
|
const FILTERS = [
|
||||||
{ key: 'all', label: 'All', test: () => true },
|
{ key: 'all', label: 'All', test: () => true },
|
||||||
@ -73,10 +80,13 @@ export async function DexGrid() {
|
|||||||
const grid = el('div', { class: 'grid' });
|
const grid = el('div', { class: 'grid' });
|
||||||
let query = '';
|
let query = '';
|
||||||
let filterKey = ui.get().filter || 'all';
|
let filterKey = ui.get().filter || 'all';
|
||||||
|
let filterType = ui.get().filterType || '';
|
||||||
|
let filterGen = Number(ui.get().filterGen) || 0;
|
||||||
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 = [];
|
||||||
let ids = [];
|
let ids = [];
|
||||||
|
let lastList = [];
|
||||||
|
|
||||||
const ring = ProgressRing();
|
const ring = ProgressRing();
|
||||||
|
|
||||||
@ -151,6 +161,52 @@ export async function DexGrid() {
|
|||||||
}
|
}
|
||||||
syncDirBtn();
|
syncDirBtn();
|
||||||
|
|
||||||
|
const typeSelect = el(
|
||||||
|
'select',
|
||||||
|
{
|
||||||
|
class: 'feed-sort__select',
|
||||||
|
onchange: (e) => {
|
||||||
|
filterType = e.target.value;
|
||||||
|
ui.set({ filterType });
|
||||||
|
paintGrid();
|
||||||
|
resetScroll();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
el('option', { value: '' }, 'Any type'),
|
||||||
|
...TYPES.map((t) => el('option', { value: t, selected: t === filterType }, prettify(t))),
|
||||||
|
);
|
||||||
|
const genSelect = el(
|
||||||
|
'select',
|
||||||
|
{
|
||||||
|
class: 'feed-sort__select',
|
||||||
|
onchange: (e) => {
|
||||||
|
filterGen = Number(e.target.value);
|
||||||
|
ui.set({ filterGen });
|
||||||
|
paintGrid();
|
||||||
|
resetScroll();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
el('option', { value: '0' }, 'Any gen'),
|
||||||
|
...[1, 2, 3, 4, 5, 6, 7, 8, 9].map((g) =>
|
||||||
|
el('option', { value: String(g), selected: g === filterGen }, `Gen ${g}`),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const randomBtn = el(
|
||||||
|
'button',
|
||||||
|
{
|
||||||
|
class: 'feed-sort__dir',
|
||||||
|
type: 'button',
|
||||||
|
title: 'Random Pokémon',
|
||||||
|
onclick: () => {
|
||||||
|
if (lastList.length) {
|
||||||
|
const pick = lastList[Math.floor(Math.random() * lastList.length)];
|
||||||
|
location.hash = `#/pokemon/${pick.species.id}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'🎲',
|
||||||
|
);
|
||||||
|
|
||||||
// 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 });
|
||||||
@ -175,6 +231,29 @@ export async function DexGrid() {
|
|||||||
if (document.fonts && document.fonts.ready) document.fonts.ready.then(go);
|
if (document.fonts && document.fonts.ready) document.fonts.ready.then(go);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const recentStrip = el('div', { class: 'recent' });
|
||||||
|
function renderRecent() {
|
||||||
|
const rec = (ui.get().recent || [])
|
||||||
|
.map((id) => snap.speciesById.get(id))
|
||||||
|
.filter(Boolean)
|
||||||
|
.slice(0, 12);
|
||||||
|
clear(recentStrip);
|
||||||
|
recentStrip.hidden = rec.length === 0;
|
||||||
|
if (!rec.length) return;
|
||||||
|
recentStrip.append(el('span', { class: 'recent__label' }, 'Recent'));
|
||||||
|
const style = settings.get().spriteStyle;
|
||||||
|
for (const sp of rec) {
|
||||||
|
recentStrip.append(
|
||||||
|
el(
|
||||||
|
'a',
|
||||||
|
{ class: 'recent__item', href: `#/pokemon/${sp.id}`, title: sp.name.replace(/-/g, ' ') },
|
||||||
|
Sprite(sp.id, { style, alt: sp.name, size: 44 }),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderRecent();
|
||||||
|
|
||||||
view.append(
|
view.append(
|
||||||
el(
|
el(
|
||||||
'header',
|
'header',
|
||||||
@ -199,7 +278,9 @@ export async function DexGrid() {
|
|||||||
sortSelect,
|
sortSelect,
|
||||||
dirBtn,
|
dirBtn,
|
||||||
),
|
),
|
||||||
|
el('div', { class: 'feed-sort feed-filters' }, typeSelect, genSelect, randomBtn),
|
||||||
),
|
),
|
||||||
|
recentStrip,
|
||||||
grid,
|
grid,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -228,9 +309,8 @@ export async function DexGrid() {
|
|||||||
function paintGrid() {
|
function paintGrid() {
|
||||||
const st = settings.get();
|
const st = settings.get();
|
||||||
const pokemonState = selection.get().pokemon;
|
const pokemonState = selection.get().pokemon;
|
||||||
const boxed =
|
const gen = snap.versionGroupByKey.get(st.versionGroup)?.generation ?? 9;
|
||||||
st.spriteStyle === 'game' &&
|
const boxed = st.spriteStyle === 'game' && gen <= 2;
|
||||||
(snap.versionGroupByKey.get(st.versionGroup)?.generation ?? 9) <= 2;
|
|
||||||
const filterTest = FILTERS.find((f) => f.key === filterKey).test;
|
const filterTest = FILTERS.find((f) => f.key === filterKey).test;
|
||||||
|
|
||||||
let list = rows.filter(({ species, number }) => {
|
let list = rows.filter(({ species, number }) => {
|
||||||
@ -242,6 +322,8 @@ export async function DexGrid() {
|
|||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (filterGen && species.generation !== filterGen) return false;
|
||||||
|
if (filterType && !typesForGen(species, gen).includes(filterType)) 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);
|
||||||
});
|
});
|
||||||
@ -255,12 +337,14 @@ export async function DexGrid() {
|
|||||||
return (a.row.number ?? a.row.species.id) - (b.row.number ?? b.row.species.id);
|
return (a.row.number ?? a.row.species.id) - (b.row.number ?? b.row.species.id);
|
||||||
})
|
})
|
||||||
.map((x) => x.row);
|
.map((x) => x.row);
|
||||||
|
lastList = list;
|
||||||
|
|
||||||
const frag = document.createDocumentFragment();
|
const frag = document.createDocumentFragment();
|
||||||
list.forEach(({ species, number }, i) => {
|
list.forEach(({ species, number }, i) => {
|
||||||
const card = Card(species, number, {
|
const card = Card(species, number, {
|
||||||
spriteStyle: st.spriteStyle,
|
spriteStyle: st.spriteStyle,
|
||||||
versionGroup: st.versionGroup,
|
versionGroup: st.versionGroup,
|
||||||
|
gen,
|
||||||
boxed,
|
boxed,
|
||||||
metric: metricLabel(sortKey, species),
|
metric: metricLabel(sortKey, species),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -15,9 +15,47 @@ import { TypeMatchups } from '../components/TypeMatchups.js';
|
|||||||
import { Locations } from '../components/Locations.js';
|
import { Locations } from '../components/Locations.js';
|
||||||
import { FlavorText } from '../components/FlavorText.js';
|
import { FlavorText } from '../components/FlavorText.js';
|
||||||
import { typeHex } from '../lib/type-color.js';
|
import { typeHex } from '../lib/type-color.js';
|
||||||
|
import { offensiveSummary } from '../data/type-chart.js';
|
||||||
|
|
||||||
const idFromUrl = (u) => Number(u.replace(/\/$/, '').split('/').pop());
|
const idFromUrl = (u) => Number(u.replace(/\/$/, '').split('/').pop());
|
||||||
|
|
||||||
|
const STAT_LABEL = {
|
||||||
|
hp: 'HP',
|
||||||
|
attack: 'Atk',
|
||||||
|
defense: 'Def',
|
||||||
|
'special-attack': 'SpA',
|
||||||
|
'special-defense': 'SpD',
|
||||||
|
speed: 'Spe',
|
||||||
|
};
|
||||||
|
const GROWTH_EXP = {
|
||||||
|
erratic: 600000,
|
||||||
|
fast: 800000,
|
||||||
|
'medium-fast': 1000000,
|
||||||
|
'medium-slow': 1059860,
|
||||||
|
slow: 1250000,
|
||||||
|
fluctuating: 1640000,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** "87.5% ♀ 12.5% ♂" or "Genderless", plus a two-tone bar. */
|
||||||
|
function genderCell(rate) {
|
||||||
|
if (rate == null || rate < 0) return el('dd', {}, 'Genderless');
|
||||||
|
const female = (rate / 8) * 100;
|
||||||
|
const bar = el('div', { class: 'gender-bar' });
|
||||||
|
bar.style.setProperty('--f', `${female}%`);
|
||||||
|
return el(
|
||||||
|
'dd',
|
||||||
|
{ class: 'gender' },
|
||||||
|
bar,
|
||||||
|
el(
|
||||||
|
'span',
|
||||||
|
{},
|
||||||
|
female < 100 ? `${(100 - female).toFixed(female % 12.5 ? 1 : 0)}% ♂` : '',
|
||||||
|
female > 0 && female < 100 ? ' · ' : '',
|
||||||
|
female > 0 ? `${female.toFixed(female % 12.5 ? 1 : 0)}% ♀` : '',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** A Pokémon's typing as it was in the selected game's generation. */
|
/** A Pokémon's typing as it was in the selected game's generation. */
|
||||||
function typesForGeneration(pokemon, gen) {
|
function typesForGeneration(pokemon, gen) {
|
||||||
const past = (pokemon.past_types || [])
|
const past = (pokemon.past_types || [])
|
||||||
@ -81,6 +119,11 @@ export async function PokemonDetail(nationalId) {
|
|||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recently-viewed history for the feed strip (most recent first, capped).
|
||||||
|
ui.set((s) => ({
|
||||||
|
recent: [nationalId, ...(s.recent || []).filter((x) => x !== nationalId)].slice(0, 12),
|
||||||
|
}));
|
||||||
|
|
||||||
const types = typesForGeneration(pokemon, vgGen);
|
const types = typesForGeneration(pokemon, vgGen);
|
||||||
const mainType = types[0];
|
const mainType = types[0];
|
||||||
view.style.setProperty('--type-main', typeHex(mainType));
|
view.style.setProperty('--type-main', typeHex(mainType));
|
||||||
@ -107,6 +150,25 @@ export async function PokemonDetail(nationalId) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const cryUrl = pokemon.cries?.latest || pokemon.cries?.legacy;
|
||||||
|
const cryBtn = cryUrl
|
||||||
|
? el(
|
||||||
|
'button',
|
||||||
|
{
|
||||||
|
class: 'phero__shiny phero__cry',
|
||||||
|
type: 'button',
|
||||||
|
title: 'Play cry',
|
||||||
|
'aria-label': 'Play cry',
|
||||||
|
onclick: () => {
|
||||||
|
const a = new Audio(cryUrl);
|
||||||
|
a.volume = 0.45;
|
||||||
|
a.play().catch(() => {});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'►',
|
||||||
|
)
|
||||||
|
: null;
|
||||||
function paintArt() {
|
function paintArt() {
|
||||||
artHolder.replaceChildren(
|
artHolder.replaceChildren(
|
||||||
Sprite(nationalId, {
|
Sprite(nationalId, {
|
||||||
@ -165,6 +227,18 @@ export async function PokemonDetail(nationalId) {
|
|||||||
.filter((a) => !a.is_hidden || vgGen >= 5)
|
.filter((a) => !a.is_hidden || vgGen >= 5)
|
||||||
.map((a) => prettify(a.ability.name) + (a.is_hidden ? ' (hidden)' : ''));
|
.map((a) => prettify(a.ability.name) + (a.is_hidden ? ' (hidden)' : ''));
|
||||||
|
|
||||||
|
const evYield =
|
||||||
|
pokemon.stats
|
||||||
|
.filter((s) => s.effort > 0)
|
||||||
|
.map((s) => `${s.effort} ${STAT_LABEL[s.stat.name] || s.stat.name}`)
|
||||||
|
.join(', ') || '—';
|
||||||
|
const heldItems =
|
||||||
|
pokemon.held_items?.map((h) => prettify(h.item.name)).join(', ') || '—';
|
||||||
|
const eggGroups = species.egg_groups.map((g) => prettify(g.name));
|
||||||
|
const hatch = species.hatch_counter;
|
||||||
|
const growth = species.growth_rate?.name;
|
||||||
|
const rate = species.capture_rate;
|
||||||
|
|
||||||
const aboutPanel = el(
|
const aboutPanel = el(
|
||||||
'div',
|
'div',
|
||||||
{ class: 'ppanel' },
|
{ class: 'ppanel' },
|
||||||
@ -183,6 +257,34 @@ export async function PokemonDetail(nationalId) {
|
|||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
|
el('h3', { class: 'ppanel__sub' }, 'Training'),
|
||||||
|
el(
|
||||||
|
'dl',
|
||||||
|
{ class: 'pfacts' },
|
||||||
|
fact('EV yield', evYield),
|
||||||
|
fact('Base EXP', pokemon.base_experience ?? '—'),
|
||||||
|
fact('Catch rate', rate != null ? `${rate} / 255 (~${Math.round((rate / 255) * 100)}% max)` : '—'),
|
||||||
|
fact('Base friendship', species.base_happiness ?? '—'),
|
||||||
|
fact(
|
||||||
|
'Growth rate',
|
||||||
|
growth ? `${prettify(growth)}${GROWTH_EXP[growth] ? ` · ${GROWTH_EXP[growth].toLocaleString()} EXP` : ''}` : '—',
|
||||||
|
),
|
||||||
|
fact('Held items', heldItems),
|
||||||
|
),
|
||||||
|
el('h3', { class: 'ppanel__sub' }, 'Breeding'),
|
||||||
|
el(
|
||||||
|
'dl',
|
||||||
|
{ class: 'pfacts' },
|
||||||
|
el('div', { class: 'fact' }, el('dt', {}, 'Gender'), genderCell(species.gender_rate)),
|
||||||
|
fact(
|
||||||
|
'Egg groups',
|
||||||
|
eggGroups.includes('Undiscovered') ? "Undiscovered (can't breed)" : eggGroups.join(', ') || '—',
|
||||||
|
),
|
||||||
|
fact(
|
||||||
|
'Egg cycles',
|
||||||
|
hatch != null ? `${hatch} (~${((hatch + 1) * 255).toLocaleString()} steps)` : '—',
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// ---- Stats panel ---------------------------------------------
|
// ---- Stats panel ---------------------------------------------
|
||||||
@ -203,6 +305,7 @@ export async function PokemonDetail(nationalId) {
|
|||||||
),
|
),
|
||||||
el('h3', { class: 'ppanel__sub' }, 'Type matchups (defending)'),
|
el('h3', { class: 'ppanel__sub' }, 'Type matchups (defending)'),
|
||||||
TypeMatchups(types, vgGen),
|
TypeMatchups(types, vgGen),
|
||||||
|
offensiveBlock(types, vgGen),
|
||||||
);
|
);
|
||||||
|
|
||||||
// ---- Evolution panel --------------------------------------
|
// ---- Evolution panel --------------------------------------
|
||||||
@ -307,7 +410,7 @@ export async function PokemonDetail(nationalId) {
|
|||||||
{ class: 'phero__top' },
|
{ class: 'phero__top' },
|
||||||
el('a', { class: 'phero__back', href: '#/' }, '‹ Dex'),
|
el('a', { class: 'phero__back', href: '#/' }, '‹ Dex'),
|
||||||
el('span', { class: 'phero__num' }, numLabel),
|
el('span', { class: 'phero__num' }, numLabel),
|
||||||
el('div', { class: 'phero__actions' }, shinyBtn, favBtn),
|
el('div', { class: 'phero__actions' }, cryBtn, shinyBtn, favBtn),
|
||||||
),
|
),
|
||||||
el(
|
el(
|
||||||
'div',
|
'div',
|
||||||
@ -372,3 +475,36 @@ async function loadEvolution(species, st, snap, vgGen) {
|
|||||||
function fact(label, value) {
|
function fact(label, value) {
|
||||||
return el('div', { class: 'fact' }, el('dt', {}, label), el('dd', {}, value));
|
return el('div', { class: 'fact' }, el('dt', {}, label), el('dd', {}, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** STAB offensive coverage summary for the Stats tab. */
|
||||||
|
function offensiveBlock(types, gen) {
|
||||||
|
const { strong, walls } = offensiveSummary(types, gen);
|
||||||
|
return el(
|
||||||
|
'div',
|
||||||
|
{},
|
||||||
|
el('h3', { class: 'ppanel__sub' }, 'STAB coverage (attacking)'),
|
||||||
|
el(
|
||||||
|
'div',
|
||||||
|
{ class: 'matchups' },
|
||||||
|
strong.length
|
||||||
|
? el(
|
||||||
|
'div',
|
||||||
|
{ class: 'matchups__row matchups__row--x2' },
|
||||||
|
el('span', { class: 'matchups__label' }, 'Strong vs'),
|
||||||
|
el('span', { class: 'matchups__types' }, ...strong.map(TypeChip)),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
walls.length
|
||||||
|
? el(
|
||||||
|
'div',
|
||||||
|
{ class: 'matchups__row matchups__row--half' },
|
||||||
|
el('span', { class: 'matchups__label' }, 'Walled by'),
|
||||||
|
el('span', { class: 'matchups__types' }, ...walls.map(TypeChip)),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
!strong.length && !walls.length
|
||||||
|
? el('p', { class: 'detail__muted' }, 'No notable STAB coverage.')
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user