Forms & variants
- Snapshot: each species gains a forms[] list (slug, form id, label, category, types, stats, BST, height, weight) built from its non-default varieties — 264 forms across 203 species (Megas, G-max, regional, alt formes; cosmetic-only forms filtered out). Snapshot ~914 KB. - Detail page: a form switcher (Base + each form). Selecting a form re-fetches its /pokemon and rebuilds the type-dependent parts — hero name/types/artwork, --type theming, stat bars + total, defensive matchups, STAB coverage, abilities, EV yield, held items, and learnset. Species-level data (flavour, breeding, evolution) stays put. - Grid cards show a '+N forms' badge. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
415cf3edec
commit
40667cbdb6
@ -71,6 +71,10 @@ 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.
|
||||||
|
- **Forms** — Megas, Gigantamax, regional forms and alternate formes (264
|
||||||
|
across 203 species) in the snapshot; a form switcher on the detail page
|
||||||
|
rebuilds types / stats / matchups / abilities / learnset / artwork for
|
||||||
|
the chosen form. Cards show a "+N forms" badge.
|
||||||
- **Detail** — type-gradient hero + tabbed sheet (About / Stats / Evolution /
|
- **Detail** — type-gradient hero + tabbed sheet (About / Stats / Evolution /
|
||||||
Moves / Locations); coloured animated stat bars; defensive type matchups;
|
Moves / Locations); coloured animated stat bars; defensive type matchups;
|
||||||
evolution chain re-parented to the selected game's generation; learnset with
|
evolution chain re-parented to the selected game's generation; learnset with
|
||||||
|
|||||||
@ -30,6 +30,34 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|||||||
const idFromUrl = (u) => Number(u.replace(/\/$/, '').split('/').pop());
|
const idFromUrl = (u) => Number(u.replace(/\/$/, '').split('/').pop());
|
||||||
const pretty = (k) => k.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
const pretty = (k) => k.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
||||||
|
|
||||||
|
const FORM_LABELS = {
|
||||||
|
mega: 'Mega',
|
||||||
|
'mega-x': 'Mega X',
|
||||||
|
'mega-y': 'Mega Y',
|
||||||
|
gmax: 'Gigantamax',
|
||||||
|
alola: 'Alolan',
|
||||||
|
galar: 'Galarian',
|
||||||
|
hisui: 'Hisuian',
|
||||||
|
paldea: 'Paldean',
|
||||||
|
primal: 'Primal',
|
||||||
|
origin: 'Origin',
|
||||||
|
altered: 'Altered',
|
||||||
|
incarnate: 'Incarnate',
|
||||||
|
therian: 'Therian',
|
||||||
|
};
|
||||||
|
function formLabel(slug, speciesName) {
|
||||||
|
const rest = slug.startsWith(`${speciesName}-`) ? slug.slice(speciesName.length + 1) : slug;
|
||||||
|
return FORM_LABELS[rest] || pretty(rest);
|
||||||
|
}
|
||||||
|
function formCategory(slug, speciesName) {
|
||||||
|
const rest = slug.startsWith(`${speciesName}-`) ? slug.slice(speciesName.length + 1) : slug;
|
||||||
|
if (rest.startsWith('mega')) return 'mega';
|
||||||
|
if (rest === 'gmax') return 'gmax';
|
||||||
|
if (['alola', 'galar', 'hisui', 'paldea'].includes(rest)) return 'regional';
|
||||||
|
if (rest === 'primal') return 'primal';
|
||||||
|
return 'other';
|
||||||
|
}
|
||||||
|
|
||||||
const _cache = new Map();
|
const _cache = new Map();
|
||||||
async function api(path) {
|
async function api(path) {
|
||||||
if (_cache.has(path)) return _cache.get(path);
|
if (_cache.has(path)) return _cache.get(path);
|
||||||
@ -152,14 +180,9 @@ async function main() {
|
|||||||
.map((r) => idFromUrl(r.url))
|
.map((r) => idFromUrl(r.url))
|
||||||
.sort((a, b) => a - b);
|
.sort((a, b) => a - b);
|
||||||
|
|
||||||
console.log(`Fetching ${ids.length} species (stats, typings, flags) …`);
|
const statMap = (pk) => {
|
||||||
const species = await mapLimit(ids, CONCURRENCY, async (id) => {
|
|
||||||
const [pk, sp] = await Promise.all([
|
|
||||||
api(`pokemon/${id}`),
|
|
||||||
api(`pokemon-species/${id}`),
|
|
||||||
]);
|
|
||||||
const s = Object.fromEntries(pk.stats.map((x) => [x.stat.name, x.base_stat]));
|
const s = Object.fromEntries(pk.stats.map((x) => [x.stat.name, x.base_stat]));
|
||||||
const stats = {
|
return {
|
||||||
hp: s.hp ?? 0,
|
hp: s.hp ?? 0,
|
||||||
atk: s.attack ?? 0,
|
atk: s.attack ?? 0,
|
||||||
def: s.defense ?? 0,
|
def: s.defense ?? 0,
|
||||||
@ -167,7 +190,56 @@ async function main() {
|
|||||||
spd: s['special-defense'] ?? 0,
|
spd: s['special-defense'] ?? 0,
|
||||||
spe: s.speed ?? 0,
|
spe: s.speed ?? 0,
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
const bstOf = (st) => st.hp + st.atk + st.def + st.spa + st.spd + st.spe;
|
||||||
|
const sameShape = (a, b) =>
|
||||||
|
a.types.join() === b.types.join() && a.bst === b.bst &&
|
||||||
|
a.stats.hp === b.stats.hp && a.stats.spe === b.stats.spe && a.stats.atk === b.stats.atk;
|
||||||
|
|
||||||
|
console.log(`Fetching ${ids.length} species (stats, typings, flags, forms) …`);
|
||||||
|
const species = await mapLimit(ids, CONCURRENCY, async (id) => {
|
||||||
|
const [pk, sp] = await Promise.all([
|
||||||
|
api(`pokemon/${id}`),
|
||||||
|
api(`pokemon-species/${id}`),
|
||||||
|
]);
|
||||||
|
const stats = statMap(pk);
|
||||||
|
const baseTypes = pk.types.slice().sort((a, b) => a.slot - b.slot).map((t) => t.type.name);
|
||||||
|
|
||||||
|
// Non-default varieties = forms (megas, G-max, regional, alt formes).
|
||||||
|
const variantSlugs = (sp.varieties || [])
|
||||||
|
.filter((v) => !v.is_default)
|
||||||
|
.map((v) => v.pokemon.name);
|
||||||
|
const forms = [];
|
||||||
|
for (const slug of variantSlugs) {
|
||||||
|
let fp;
|
||||||
|
try {
|
||||||
|
fp = await api(`pokemon/${slug}`);
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const fstats = statMap(fp);
|
||||||
|
const ftypes = fp.types.slice().sort((a, b) => a.slot - b.slot).map((t) => t.type.name);
|
||||||
|
const cat = formCategory(slug, sp.name);
|
||||||
|
const entry = {
|
||||||
|
slug,
|
||||||
|
id: fp.id,
|
||||||
|
name: formLabel(slug, sp.name),
|
||||||
|
category: cat,
|
||||||
|
types: ftypes,
|
||||||
|
stats: fstats,
|
||||||
|
bst: bstOf(fstats),
|
||||||
|
height: fp.height,
|
||||||
|
weight: fp.weight,
|
||||||
|
};
|
||||||
|
// Drop purely-cosmetic forms (identical stats & typing, no named category).
|
||||||
|
if (cat === 'other' && sameShape(entry, { types: baseTypes, bst: bstOf(stats), stats })) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
forms.push(entry);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
forms,
|
||||||
id,
|
id,
|
||||||
name: sp.name,
|
name: sp.name,
|
||||||
generation: idFromUrl(sp.generation.url),
|
generation: idFromUrl(sp.generation.url),
|
||||||
|
|||||||
@ -61,6 +61,9 @@ export function Card(species, number, { spriteStyle = 'official', versionGroup,
|
|||||||
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' }, ...types.map(TypeChip)),
|
el('span', { class: 'card__types' }, ...types.map(TypeChip)),
|
||||||
|
species.forms?.length
|
||||||
|
? el('span', { class: 'card__forms' }, `+${species.forms.length} form${species.forms.length > 1 ? 's' : ''}`)
|
||||||
|
: null,
|
||||||
metric ? el('span', { class: 'card__metric' }, metric) : null,
|
metric ? el('span', { class: 'card__metric' }, metric) : null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2390,3 +2390,43 @@
|
|||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Forms ------------------------------------------------- */
|
||||||
|
.phero__form {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.62em;
|
||||||
|
opacity: 0.85;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
.phero__form:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.formbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
.formbar__pill {
|
||||||
|
appearance: none;
|
||||||
|
border: 1.5px solid rgba(255, 255, 255, 0.35);
|
||||||
|
background: rgba(255, 255, 255, 0.14);
|
||||||
|
color: var(--ink);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.74rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 5px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.formbar__pill.is-active {
|
||||||
|
background: #fff;
|
||||||
|
border-color: #fff;
|
||||||
|
color: var(--type-main);
|
||||||
|
}
|
||||||
|
.card__forms {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 0.62rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: color-mix(in srgb, var(--type-main) 55%, var(--text-dim));
|
||||||
|
}
|
||||||
|
|||||||
@ -125,11 +125,21 @@ export async function PokemonDetail(nationalId) {
|
|||||||
recent: [nationalId, ...(s.recent || []).filter((x) => x !== nationalId)].slice(0, 12),
|
recent: [nationalId, ...(s.recent || []).filter((x) => x !== nationalId)].slice(0, 12),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const types = typesForGeneration(pokemon, vgGen);
|
// Form state: null = the base Pokémon; otherwise a snapshot form entry
|
||||||
const mainType = types[0];
|
// whose full /pokemon data lives in `pk`.
|
||||||
view.style.setProperty('--type-main', typeHex(mainType));
|
const snapSpecies = snap.speciesById.get(nationalId);
|
||||||
view.style.setProperty('--type-2', typeHex(types[1] || mainType));
|
const forms = (snapSpecies && snapSpecies.forms) || [];
|
||||||
view.dataset.type = mainType;
|
let activeForm = null;
|
||||||
|
let pk = pokemon;
|
||||||
|
|
||||||
|
let types = typesForGeneration(pk, vgGen);
|
||||||
|
let mainType = types[0];
|
||||||
|
function applyThemeVars() {
|
||||||
|
view.style.setProperty('--type-main', typeHex(mainType));
|
||||||
|
view.style.setProperty('--type-2', typeHex(types[1] || mainType));
|
||||||
|
view.dataset.type = mainType;
|
||||||
|
}
|
||||||
|
applyThemeVars();
|
||||||
|
|
||||||
// ---- Artwork + shiny --------------------------------------------
|
// ---- Artwork + shiny --------------------------------------------
|
||||||
// Shiny Pokémon were introduced in Gen 2 — no toggle for Gen 1 games.
|
// Shiny Pokémon were introduced in Gen 2 — no toggle for Gen 1 games.
|
||||||
@ -172,7 +182,7 @@ export async function PokemonDetail(nationalId) {
|
|||||||
: null;
|
: null;
|
||||||
function paintArt() {
|
function paintArt() {
|
||||||
artHolder.replaceChildren(
|
artHolder.replaceChildren(
|
||||||
Sprite(nationalId, {
|
Sprite(activeForm ? activeForm.id : nationalId, {
|
||||||
style: artStyle,
|
style: artStyle,
|
||||||
shiny,
|
shiny,
|
||||||
versionGroup: st.versionGroup,
|
versionGroup: st.versionGroup,
|
||||||
@ -241,65 +251,108 @@ export async function PokemonDetail(nationalId) {
|
|||||||
}
|
}
|
||||||
syncTrack();
|
syncTrack();
|
||||||
|
|
||||||
// ---- About panel -----------------------------------------------
|
const allGames = st.versionGroup === 'all';
|
||||||
const abilityList = pokemon.abilities.filter((a) => !a.is_hidden || vgGen >= 5);
|
let statBars = [];
|
||||||
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 =
|
// ---- Form-dependent content (rebuilt when a form is selected) -----
|
||||||
pokemon.stats
|
function formFactsNode(p) {
|
||||||
.filter((s) => s.effort > 0)
|
const abilityList = p.abilities.filter((a) => !a.is_hidden || vgGen >= 5);
|
||||||
.map((s) => `${s.effort} ${STAT_LABEL[s.stat.name] || s.stat.name}`)
|
const abilitiesNode = el(
|
||||||
.join(', ') || '—';
|
'span',
|
||||||
const heldItems =
|
{},
|
||||||
pokemon.held_items?.map((h) => prettify(h.item.name)).join(', ') || '—';
|
...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 =
|
||||||
|
p.stats.filter((s) => s.effort > 0).map((s) => `${s.effort} ${STAT_LABEL[s.stat.name] || s.stat.name}`).join(', ') ||
|
||||||
|
'—';
|
||||||
|
const heldItems = p.held_items?.map((h) => prettify(h.item.name)).join(', ') || '—';
|
||||||
|
return el(
|
||||||
|
'div',
|
||||||
|
{},
|
||||||
|
el(
|
||||||
|
'dl',
|
||||||
|
{ class: 'pfacts' },
|
||||||
|
fact('Height', `${(p.height / 10).toFixed(1)} m`),
|
||||||
|
fact('Weight', `${(p.weight / 10).toFixed(1)} kg`),
|
||||||
|
abilityList.length && vgGen >= 3 ? fact('Abilities', abilitiesNode) : null,
|
||||||
|
fact('Introduced', prettify(species.generation.name)),
|
||||||
|
species.genera?.length
|
||||||
|
? fact('Category', (species.genera.find((g) => g.language.name === 'en') || {}).genus || '—')
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
el('h3', { class: 'ppanel__sub' }, 'Training'),
|
||||||
|
el(
|
||||||
|
'dl',
|
||||||
|
{ class: 'pfacts' },
|
||||||
|
fact('EV yield', evYield),
|
||||||
|
fact('Base EXP', p.base_experience ?? '—'),
|
||||||
|
fact(
|
||||||
|
'Catch rate',
|
||||||
|
species.capture_rate != null
|
||||||
|
? `${species.capture_rate} / 255 (~${Math.round((species.capture_rate / 255) * 100)}% max)`
|
||||||
|
: '—',
|
||||||
|
),
|
||||||
|
fact('Base friendship', species.base_happiness ?? '—'),
|
||||||
|
fact(
|
||||||
|
'Growth rate',
|
||||||
|
species.growth_rate?.name
|
||||||
|
? `${prettify(species.growth_rate.name)}${
|
||||||
|
GROWTH_EXP[species.growth_rate.name]
|
||||||
|
? ` · ${GROWTH_EXP[species.growth_rate.name].toLocaleString()} EXP`
|
||||||
|
: ''
|
||||||
|
}`
|
||||||
|
: '—',
|
||||||
|
),
|
||||||
|
fact('Held items', heldItems),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function statsContent(p) {
|
||||||
|
const stat = (name) => p.stats.find((s) => s.stat.name === name)?.base_stat ?? 0;
|
||||||
|
const total = STAT_NAMES.reduce((sum, n) => sum + stat(n), 0);
|
||||||
|
statBars = STAT_NAMES.map((n) => StatBar(n, stat(n)));
|
||||||
|
return [
|
||||||
|
el('div', { class: 'pstats' }, ...statBars),
|
||||||
|
el(
|
||||||
|
'div',
|
||||||
|
{ class: 'statbar statbar--total' },
|
||||||
|
el('span', { class: 'statbar__label' }, 'Total'),
|
||||||
|
el('span', { class: 'statbar__value' }, String(total)),
|
||||||
|
el('div', { class: 'statbar__track' }),
|
||||||
|
),
|
||||||
|
el('h3', { class: 'ppanel__sub' }, 'Type matchups (defending)'),
|
||||||
|
TypeMatchups(types, vgGen),
|
||||||
|
offensiveBlock(types, vgGen),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function movesContent(p) {
|
||||||
|
return allGames
|
||||||
|
? el('p', { class: 'detail__muted' }, 'Pick a game to see its learnset.')
|
||||||
|
: MovesList(p.moves, {
|
||||||
|
versionGroupKey: st.versionGroup,
|
||||||
|
gen: vgGen,
|
||||||
|
genOfVg: (name) => snap.versionGroupByKey.get(name)?.generation ?? 9,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- About panel (FlavorText + form facts + species breeding) ----
|
||||||
const eggGroups = species.egg_groups.map((g) => prettify(g.name));
|
const eggGroups = species.egg_groups.map((g) => prettify(g.name));
|
||||||
const hatch = species.hatch_counter;
|
const hatch = species.hatch_counter;
|
||||||
const growth = species.growth_rate?.name;
|
const factsHolder = el('div', {}, formFactsNode(pk));
|
||||||
const rate = species.capture_rate;
|
|
||||||
|
|
||||||
const aboutPanel = el(
|
const aboutPanel = el(
|
||||||
'div',
|
'div',
|
||||||
{ class: 'ppanel' },
|
{ class: 'ppanel' },
|
||||||
FlavorText(species, vg),
|
FlavorText(species, vg),
|
||||||
el(
|
factsHolder,
|
||||||
'dl',
|
|
||||||
{ class: 'pfacts' },
|
|
||||||
fact('Height', `${(pokemon.height / 10).toFixed(1)} m`),
|
|
||||||
fact('Weight', `${(pokemon.weight / 10).toFixed(1)} kg`),
|
|
||||||
abilityList.length && vgGen >= 3 ? fact('Abilities', abilitiesNode) : null,
|
|
||||||
fact('Introduced', prettify(species.generation.name)),
|
|
||||||
species.genera?.length
|
|
||||||
? fact(
|
|
||||||
'Category',
|
|
||||||
(species.genera.find((g) => g.language.name === 'en') || {}).genus || '—',
|
|
||||||
)
|
|
||||||
: 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('h3', { class: 'ppanel__sub' }, 'Breeding'),
|
||||||
el(
|
el(
|
||||||
'dl',
|
'dl',
|
||||||
@ -316,45 +369,14 @@ export async function PokemonDetail(nationalId) {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// ---- Stats panel ---------------------------------------------
|
const statsPanel = el('div', { class: 'ppanel' }, ...statsContent(pk));
|
||||||
const stat = (name) =>
|
|
||||||
pokemon.stats.find((s) => s.stat.name === name)?.base_stat ?? 0;
|
|
||||||
const statTotal = STAT_NAMES.reduce((sum, n) => sum + stat(n), 0);
|
|
||||||
const statBars = STAT_NAMES.map((n) => StatBar(n, stat(n)));
|
|
||||||
const statsPanel = el(
|
|
||||||
'div',
|
|
||||||
{ class: 'ppanel' },
|
|
||||||
el('div', { class: 'pstats' }, ...statBars),
|
|
||||||
el(
|
|
||||||
'div',
|
|
||||||
{ class: 'statbar statbar--total' },
|
|
||||||
el('span', { class: 'statbar__label' }, 'Total'),
|
|
||||||
el('span', { class: 'statbar__value' }, String(statTotal)),
|
|
||||||
el('div', { class: 'statbar__track' }),
|
|
||||||
),
|
|
||||||
el('h3', { class: 'ppanel__sub' }, 'Type matchups (defending)'),
|
|
||||||
TypeMatchups(types, vgGen),
|
|
||||||
offensiveBlock(types, vgGen),
|
|
||||||
);
|
|
||||||
|
|
||||||
// ---- Evolution panel --------------------------------------
|
// ---- Evolution panel --------------------------------------
|
||||||
const evoPanel = el('div', { class: 'ppanel' }, el('p', { class: 'detail__muted' }, 'Loading…'));
|
const evoPanel = el('div', { class: 'ppanel' }, el('p', { class: 'detail__muted' }, 'Loading…'));
|
||||||
loadEvolution(species, st, snap, vgGen).then((node) => evoPanel.replaceChildren(node));
|
loadEvolution(species, st, snap, vgGen).then((node) => evoPanel.replaceChildren(node));
|
||||||
|
|
||||||
const allGames = st.versionGroup === 'all';
|
|
||||||
|
|
||||||
// ---- Moves panel ----------------------------------------
|
// ---- Moves panel ----------------------------------------
|
||||||
const movesPanel = el(
|
const movesPanel = el('div', { class: 'ppanel' }, movesContent(pk));
|
||||||
'div',
|
|
||||||
{ class: 'ppanel' },
|
|
||||||
allGames
|
|
||||||
? el('p', { class: 'detail__muted' }, 'Pick a game to see its learnset.')
|
|
||||||
: MovesList(pokemon.moves, {
|
|
||||||
versionGroupKey: st.versionGroup,
|
|
||||||
gen: vgGen,
|
|
||||||
genOfVg: (name) => snap.versionGroupByKey.get(name)?.generation ?? 9,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// ---- Locations panel ----------------------------------
|
// ---- Locations panel ----------------------------------
|
||||||
const locPanel = el('div', { class: 'ppanel' }, el('p', { class: 'detail__muted' }, 'Loading…'));
|
const locPanel = el('div', { class: 'ppanel' }, el('p', { class: 'detail__muted' }, 'Loading…'));
|
||||||
@ -427,6 +449,55 @@ export async function PokemonDetail(nationalId) {
|
|||||||
? `#${String(regionalNumber).padStart(3, '0')} · ${prettify(dex.name || dex.key)}`
|
? `#${String(regionalNumber).padStart(3, '0')} · ${prettify(dex.name || dex.key)}`
|
||||||
: `#${String(nationalId).padStart(4, '0')} · National`;
|
: `#${String(nationalId).padStart(4, '0')} · National`;
|
||||||
|
|
||||||
|
const heroForm = el('span', { class: 'phero__form' });
|
||||||
|
const heroName = el(
|
||||||
|
'h1',
|
||||||
|
{ class: 'phero__name' },
|
||||||
|
el('span', {}, species.name.replace(/-/g, ' ')),
|
||||||
|
heroForm,
|
||||||
|
);
|
||||||
|
const heroTypes = el('div', { class: 'phero__types' }, ...types.map(TypeChip));
|
||||||
|
|
||||||
|
// Form switcher: Base + each snapshot form. Selecting one re-fetches that
|
||||||
|
// form's /pokemon and rebuilds the type-dependent parts of the page.
|
||||||
|
let formBar = null;
|
||||||
|
if (forms.length) {
|
||||||
|
const opts = [{ slug: null, name: 'Base' }, ...forms];
|
||||||
|
const pills = opts.map((o) =>
|
||||||
|
el(
|
||||||
|
'button',
|
||||||
|
{
|
||||||
|
class: 'formbar__pill',
|
||||||
|
type: 'button',
|
||||||
|
onclick: () => switchForm(o.slug ? o : null, pills, opts),
|
||||||
|
},
|
||||||
|
o.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
pills[0].classList.add('is-active');
|
||||||
|
formBar = el('div', { class: 'formbar' }, ...pills);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function switchForm(form, pills, opts) {
|
||||||
|
activeForm = form;
|
||||||
|
pills.forEach((b, i) => b.classList.toggle('is-active', opts[i].slug === (form ? form.slug : null)));
|
||||||
|
heroForm.textContent = form ? form.name : '';
|
||||||
|
try {
|
||||||
|
pk = form ? await getPokemon(form.slug) : pokemon;
|
||||||
|
} catch {
|
||||||
|
pk = pokemon;
|
||||||
|
}
|
||||||
|
types = typesForGeneration(pk, vgGen);
|
||||||
|
mainType = types[0];
|
||||||
|
applyThemeVars();
|
||||||
|
paintArt();
|
||||||
|
heroTypes.replaceChildren(...types.map(TypeChip));
|
||||||
|
factsHolder.replaceChildren(formFactsNode(pk));
|
||||||
|
statsPanel.replaceChildren(...statsContent(pk));
|
||||||
|
movesPanel.replaceChildren(movesContent(pk));
|
||||||
|
if (ui.get().detailTab === 'stats') animateStats();
|
||||||
|
}
|
||||||
|
|
||||||
clear(view).append(
|
clear(view).append(
|
||||||
el(
|
el(
|
||||||
'div',
|
'div',
|
||||||
@ -444,9 +515,10 @@ export async function PokemonDetail(nationalId) {
|
|||||||
el(
|
el(
|
||||||
'div',
|
'div',
|
||||||
{ class: 'phero__head' },
|
{ class: 'phero__head' },
|
||||||
el('h1', { class: 'phero__name' }, species.name.replace(/-/g, ' ')),
|
heroName,
|
||||||
el('div', { class: 'phero__types' }, ...types.map(TypeChip)),
|
heroTypes,
|
||||||
),
|
),
|
||||||
|
formBar,
|
||||||
el(
|
el(
|
||||||
'div',
|
'div',
|
||||||
{ class: 'phero__stage' },
|
{ class: 'phero__stage' },
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user