dex/src/views/BreedingView.js
chris b15ee3dd7f Shiny hunt tracker (#/shiny)
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
2026-09-09 19:06:46 -04:00

230 lines
7.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 cant 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;
}