Breeding helper (#/breeding)
Two modes, linked from the Team header: - Egg groups — browse every species in each of the 14 breedable egg groups (offline, from the snapshot's eggGroups). - Can breed with — pick a Pokémon and get its compatible partners (shared egg group; Ditto pairs with everything; genderless pairs only with Ditto; Undiscovered can't breed), plus its family's egg moves (walks evolves-from to the base stage — Pikachu shows Pichu's), each linking to the move page (which already lists who can learn it). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
This commit is contained in:
parent
53f4f4381d
commit
48cb471489
@ -11,6 +11,7 @@ import { NaturesView } from './views/NaturesView.js';
|
|||||||
import { TypeChartView } from './views/TypeChartView.js';
|
import { TypeChartView } from './views/TypeChartView.js';
|
||||||
import { CompareView } from './views/CompareView.js';
|
import { CompareView } from './views/CompareView.js';
|
||||||
import { ProgressView } from './views/ProgressView.js';
|
import { ProgressView } from './views/ProgressView.js';
|
||||||
|
import { BreedingView } from './views/BreedingView.js';
|
||||||
import { detailSkeleton, lookupSkeleton } from './components/skeletons.js';
|
import { detailSkeleton, lookupSkeleton } from './components/skeletons.js';
|
||||||
import { prefersReducedMotion } from './store/settings.js';
|
import { prefersReducedMotion } from './store/settings.js';
|
||||||
|
|
||||||
@ -25,6 +26,7 @@ const routes = [
|
|||||||
{ pattern: /^#\/types$/, view: () => TypeChartView() },
|
{ pattern: /^#\/types$/, view: () => TypeChartView() },
|
||||||
{ pattern: /^#\/compare$/, view: () => CompareView() },
|
{ pattern: /^#\/compare$/, view: () => CompareView() },
|
||||||
{ pattern: /^#\/progress$/, view: () => ProgressView() },
|
{ pattern: /^#\/progress$/, view: () => ProgressView() },
|
||||||
|
{ pattern: /^#\/breeding$/, view: () => BreedingView() },
|
||||||
{ pattern: /^#\/search$/, view: () => SearchView() },
|
{ pattern: /^#\/search$/, view: () => SearchView() },
|
||||||
{ pattern: /^#\/settings$/, view: () => SettingsView() },
|
{ pattern: /^#\/settings$/, view: () => SettingsView() },
|
||||||
];
|
];
|
||||||
|
|||||||
@ -23,6 +23,9 @@ export const ui = createStore('pdx.ui', {
|
|||||||
compareIds: [],
|
compareIds: [],
|
||||||
toolsOpen: false,
|
toolsOpen: false,
|
||||||
teamMode: 'coverage',
|
teamMode: 'coverage',
|
||||||
|
breedMode: 'groups',
|
||||||
|
breedGroup: '',
|
||||||
|
breedTarget: 0,
|
||||||
mvType: '',
|
mvType: '',
|
||||||
mvClass: '',
|
mvClass: '',
|
||||||
mvSort: 'name',
|
mvSort: 'name',
|
||||||
|
|||||||
@ -3099,3 +3099,66 @@
|
|||||||
}
|
}
|
||||||
.prog__key--caught { background: var(--good); }
|
.prog__key--caught { background: var(--good); }
|
||||||
.prog__key--seen { background: color-mix(in srgb, var(--accent) 35%, transparent); }
|
.prog__key--seen { background: color-mix(in srgb, var(--accent) 35%, transparent); }
|
||||||
|
|
||||||
|
/* ---- Breeding helper --------------------------------------- */
|
||||||
|
.breedview__body {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
.breed__chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.breed__chip {
|
||||||
|
border: 1.5px solid var(--border);
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 5px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.breed__chip.is-on {
|
||||||
|
background: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.breed__pick {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1.5px solid var(--border);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: var(--surface-2);
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.breed__pick span {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.breed__note {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.breed__eggmoves {
|
||||||
|
margin: 8px 0 16px;
|
||||||
|
}
|
||||||
|
.breed__eggmove-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px 14px;
|
||||||
|
margin-top: 6px;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|||||||
226
src/views/BreedingView.js
Normal file
226
src/views/BreedingView.js
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
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 = el(
|
||||||
|
'div',
|
||||||
|
{ class: 'seg' },
|
||||||
|
...[
|
||||||
|
['groups', 'Egg groups'],
|
||||||
|
['compat', 'Can breed with'],
|
||||||
|
].map(([id, label]) =>
|
||||||
|
el(
|
||||||
|
'button',
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
class: `seg__btn${id === mode ? ' is-active' : ''}`,
|
||||||
|
onclick: () => {
|
||||||
|
mode = id;
|
||||||
|
ui.set({ breedMode: id });
|
||||||
|
[...seg.children].forEach((b, i) => b.classList.toggle('is-active', ['groups', 'compat'][i] === id));
|
||||||
|
render();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
label,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
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();
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
}
|
||||||
@ -70,6 +70,8 @@ export async function TeamView() {
|
|||||||
el('a', { class: 'link', href: '#/types' }, 'Type chart'),
|
el('a', { class: 'link', href: '#/types' }, 'Type chart'),
|
||||||
' · ',
|
' · ',
|
||||||
el('a', { class: 'link', href: '#/compare' }, 'Compare'),
|
el('a', { class: 'link', href: '#/compare' }, 'Compare'),
|
||||||
|
' · ',
|
||||||
|
el('a', { class: 'link', href: '#/breeding' }, 'Breeding'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
seg,
|
seg,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user