New store + view (linked from Settings > Your data): one card per active hunt with a big +1 counter, the method's odds, and the running cumulative chance (1 - (1-p)^count). Nine methods with shiny-charm variants (Full odds, Masuda, SOS, Radar, DexNav, Catch Combo, Dynamax Adventure, Mass/Massive Outbreak). "Found it" marks the Pokémon caught and clears the hunt; per-hunt notes field. Also: JSON export/import now covers team, formTracking and shinyHunts alongside settings/selection; openPokemonPicker gains a `closeAfterPick` option (used by the shiny + breeding single-pick flows). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
230 lines
7.2 KiB
JavaScript
230 lines
7.2 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';
|
||
|
||
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();
|
||
},
|
||
{ 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;
|
||
}
|