New lib/anim.js (countUp, fadeSlideIn, segThumb), all reduced-motion aware: - Shared-element morph: tapping a card tags its art with view-transition-name; the detail skeleton now renders the real sprite at hero size (router passes the match, animates the skeleton mount), so the sprite flies from the grid into the hero and the skeleton→content swap is seamless. - Segmented controls (Team / Search / Breeding) get a sliding pill that measures each button and eases between them. - Count-ups: the dex-feed progress number and the detail Stats total tick up. - Detail tab content fades/rises on switch. - Global :active scale-press on buttons/chips/rows, plus soft background/border transitions. - Hero sprite has a slow idle bob. - Router swallows the AbortError when a fast follow-up nav skips an in-flight view transition. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
236 lines
7.4 KiB
JavaScript
236 lines
7.4 KiB
JavaScript
import { el, clear, onTeardown } from '../lib/dom.js';
|
||
import { loadSnapshot } from '../data/snapshot.js';
|
||
import { settings } from '../store/settings.js';
|
||
import { ui } from '../store/ui.js';
|
||
import { getPokemon } from '../data/api.js';
|
||
import { prettify } from '../data/pokedex-resolver.js';
|
||
import { Sprite } from '../components/Sprite.js';
|
||
import { openPokemonPicker } from '../components/PokemonPicker.js';
|
||
import { segThumb } from '../lib/anim.js';
|
||
|
||
const EGG_LABEL = {
|
||
water1: 'Water 1', water2: 'Water 2', water3: 'Water 3',
|
||
ground: 'Field', humanshape: 'Human-Like', indeterminate: 'Amorphous',
|
||
plant: 'Grass', 'no-eggs': 'Undiscovered', ditto: 'Ditto',
|
||
};
|
||
const eggLabel = (g) => EGG_LABEL[g] || prettify(g);
|
||
const idFromUrl = (u) => Number(u.replace(/\/$/, '').split('/').pop());
|
||
|
||
export async function BreedingView() {
|
||
const view = el('section', { class: 'view lookup breedview' });
|
||
const snap = await loadSnapshot();
|
||
const style = settings.get().spriteStyle;
|
||
const vg = settings.get().versionGroup;
|
||
|
||
const groups = [...new Set(snap.species.flatMap((s) => s.eggGroups || []))]
|
||
.filter((g) => g !== 'no-eggs')
|
||
.sort((a, b) => eggLabel(a).localeCompare(eggLabel(b)));
|
||
|
||
let mode = ui.get().breedMode === 'compat' ? 'compat' : 'groups';
|
||
let group = groups.includes(ui.get().breedGroup) ? ui.get().breedGroup : groups[0];
|
||
let target = snap.speciesById.get(ui.get().breedTarget) || null;
|
||
|
||
const SEG = [
|
||
['groups', 'Egg groups'],
|
||
['compat', 'Can breed with'],
|
||
];
|
||
const seg = el(
|
||
'div',
|
||
{ class: 'seg' },
|
||
...SEG.map(([id, label], i) =>
|
||
el(
|
||
'button',
|
||
{
|
||
type: 'button',
|
||
class: `seg__btn${id === mode ? ' is-active' : ''}`,
|
||
onclick: () => {
|
||
mode = id;
|
||
ui.set({ breedMode: id });
|
||
seg
|
||
.querySelectorAll('.seg__btn')
|
||
.forEach((b, j) => b.classList.toggle('is-active', SEG[j][0] === id));
|
||
setThumb(i);
|
||
render();
|
||
},
|
||
},
|
||
label,
|
||
),
|
||
),
|
||
);
|
||
const setThumb = segThumb(seg, SEG.length, Math.max(0, SEG.findIndex(([id]) => id === mode)));
|
||
|
||
const body = el('div', { class: 'breedview__body' });
|
||
|
||
function monGrid(list) {
|
||
return list.length
|
||
? el(
|
||
'div',
|
||
{ class: 'ab-mons' },
|
||
...list.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.');
|
||
}
|
||
|
||
function renderGroups() {
|
||
const chips = el(
|
||
'div',
|
||
{ class: 'breed__chips' },
|
||
...groups.map((g) =>
|
||
el(
|
||
'button',
|
||
{
|
||
type: 'button',
|
||
class: `breed__chip${g === group ? ' is-on' : ''}`,
|
||
onclick: () => {
|
||
group = g;
|
||
ui.set({ breedGroup: g });
|
||
render();
|
||
},
|
||
},
|
||
eggLabel(g),
|
||
),
|
||
),
|
||
);
|
||
const list = snap.species
|
||
.filter((s) => (s.eggGroups || []).includes(group))
|
||
.sort((a, b) => a.id - b.id);
|
||
body.append(
|
||
chips,
|
||
el('h2', { class: 'ppanel__sub' }, `${eggLabel(group)} (${list.length})`),
|
||
monGrid(list),
|
||
);
|
||
}
|
||
|
||
function renderCompat() {
|
||
const slot = el(
|
||
'button',
|
||
{
|
||
type: 'button',
|
||
class: 'breed__pick',
|
||
onclick: () =>
|
||
openPokemonPicker(
|
||
(id) => {
|
||
target = snap.speciesById.get(id);
|
||
ui.set({ breedTarget: id });
|
||
render();
|
||
},
|
||
{ closeAfterPick: true },
|
||
),
|
||
},
|
||
target
|
||
? el('span', {}, Sprite(target.id, { style, size: 40, alt: target.name }), ` ${prettify(target.name)}`)
|
||
: 'Choose a Pokémon…',
|
||
);
|
||
body.append(slot);
|
||
if (!target) return;
|
||
|
||
const tGroups = new Set(target.eggGroups || []);
|
||
if (tGroups.has('no-eggs')) {
|
||
body.append(el('p', { class: 'detail__muted' }, `${prettify(target.name)} is in the Undiscovered egg group — it can’t be bred.`));
|
||
return;
|
||
}
|
||
const isDitto = tGroups.has('ditto');
|
||
const genderless = target.genderRate === -1;
|
||
|
||
let list;
|
||
if (isDitto) {
|
||
list = snap.species.filter((s) => !s.eggGroups.includes('no-eggs') && !s.eggGroups.includes('ditto'));
|
||
} else if (genderless) {
|
||
list = snap.species.filter((s) => (s.eggGroups || []).includes('ditto'));
|
||
} else {
|
||
list = snap.species.filter((s) => {
|
||
if (s.id === target.id) return false;
|
||
if ((s.eggGroups || []).includes('ditto')) return true;
|
||
if ((s.eggGroups || []).includes('no-eggs')) return false;
|
||
return (s.eggGroups || []).some((g) => tGroups.has(g) && g !== 'ditto');
|
||
});
|
||
}
|
||
list.sort((a, b) => a.id - b.id);
|
||
|
||
const note = genderless
|
||
? 'Genderless — can only breed with Ditto.'
|
||
: isDitto
|
||
? 'Ditto breeds with any Pokémon that can breed.'
|
||
: `Egg groups: ${[...tGroups].map(eggLabel).join(', ')}. Needs an opposite-gender partner (or Ditto).`;
|
||
|
||
// Egg moves belong to the family's lowest breeding stage (Pichu, not
|
||
// Pikachu) — walk down the evolves-from chain.
|
||
let base = target;
|
||
while (base.evolvesFromId) {
|
||
const prev = snap.speciesById.get(base.evolvesFromId);
|
||
if (!prev) break;
|
||
base = prev;
|
||
}
|
||
const eggMovesBox = el('div', { class: 'breed__eggmoves' }, el('p', { class: 'detail__muted' }, 'Loading egg moves…'));
|
||
getPokemon(base.id)
|
||
.then((pk) => {
|
||
const eggMoves = pk.moves.filter((m) =>
|
||
m.version_group_details.some(
|
||
(d) => d.move_learn_method.name === 'egg' && (vg === 'all' || d.version_group.name === vg),
|
||
),
|
||
);
|
||
clear(eggMovesBox);
|
||
if (!eggMoves.length) {
|
||
eggMovesBox.append(el('p', { class: 'detail__muted' }, 'No egg moves for this game.'));
|
||
return;
|
||
}
|
||
eggMovesBox.append(
|
||
el(
|
||
'h3',
|
||
{ class: 'ppanel__sub' },
|
||
`Egg moves (${eggMoves.length})${base.id !== target.id ? ` — hatches as ${prettify(base.name)}` : ''}`,
|
||
),
|
||
el(
|
||
'p',
|
||
{ class: 'detail__muted' },
|
||
'A parent that knows one of these passes it down. Open a move to see which Pokémon can learn it.',
|
||
),
|
||
el(
|
||
'div',
|
||
{ class: 'breed__eggmove-list' },
|
||
...eggMoves.map((m) =>
|
||
el('a', { class: 'link', href: `#/move/${idFromUrl(m.move.url)}` }, m.move.name.replace(/-/g, ' ')),
|
||
),
|
||
),
|
||
);
|
||
})
|
||
.catch(() => clear(eggMovesBox).append(el('p', { class: 'detail__muted' }, 'Egg moves unavailable offline.')));
|
||
|
||
body.append(
|
||
el('p', { class: 'breed__note' }, note),
|
||
eggMovesBox,
|
||
el('h2', { class: 'ppanel__sub' }, `Compatible partners (${list.length})`),
|
||
monGrid(list),
|
||
);
|
||
}
|
||
|
||
function render() {
|
||
clear(body);
|
||
if (mode === 'groups') renderGroups();
|
||
else renderCompat();
|
||
}
|
||
|
||
clear(view).append(
|
||
el('nav', { class: 'lookup__nav' }, el('a', { class: 'link', href: '#/team' }, '‹ Team')),
|
||
el(
|
||
'header',
|
||
{ class: 'view__header' },
|
||
el('h1', {}, 'Breeding'),
|
||
el('p', {}, 'Egg-group members and who a Pokémon can produce eggs with.'),
|
||
),
|
||
seg,
|
||
body,
|
||
);
|
||
render();
|
||
|
||
onTeardown(view, () => {});
|
||
return view;
|
||
}
|