Compare commits

..

No commits in common. "7f12f087187806b85a48fa214762fc8588015477" and "53f4f4381daeaed015a079221a32775a2796ec68" have entirely different histories.

13 changed files with 27 additions and 721 deletions

View File

@ -18,8 +18,7 @@ framework.
- **Detail data** (stats, abilities, flavor text, evolution) → fetched lazily from `pokeapi.co` per Pokémon, then cached by the service worker (stale-while-revalidate, 30-day TTL).
- **Sprites** → cache-first with a capped LRU.
Export / import of all local state (settings, tracking, team, form
tracking, shiny hunts) as JSON lives in **Settings**.
Export / import of `settings` + `selection` as JSON lives in **Settings**.
## Scripts

View File

@ -31,7 +31,7 @@ export function CompareTable(members, { gen = 9, style } = {}) {
el(
'a',
{ class: 'cmp__mem', href: `#/pokemon/${sp.id}` },
Sprite(sp.spriteId ?? sp.id, { style, size: 44, alt: sp.name }),
Sprite(sp.id, { style, size: 44, alt: sp.name }),
el('span', {}, sp.name.replace(/-/g, ' ')),
),
),

View File

@ -11,7 +11,7 @@ let openInstance = null;
* `onPick(id)` fires per selection; the sheet stays open so you can add
* several. Dismiss with the backdrop, , or Escape.
*/
export async function openPokemonPicker(onPick, { closeAfterPick = false } = {}) {
export async function openPokemonPicker(onPick) {
if (openInstance) return;
const snap = await loadSnapshot();
@ -69,10 +69,7 @@ export async function openPokemonPicker(onPick, { closeAfterPick = false } = {})
{
class: 'picker__row',
type: 'button',
onclick: () => {
onPick(s.id);
if (closeAfterPick) close();
},
onclick: () => onPick(s.id),
},
Sprite(s.id, { style, alt: s.name, size: 44 }),
el('span', { class: 'search__num' }, `#${String(s.id).padStart(4, '0')}`),

View File

@ -11,8 +11,6 @@ import { NaturesView } from './views/NaturesView.js';
import { TypeChartView } from './views/TypeChartView.js';
import { CompareView } from './views/CompareView.js';
import { ProgressView } from './views/ProgressView.js';
import { BreedingView } from './views/BreedingView.js';
import { ShinyView } from './views/ShinyView.js';
import { detailSkeleton, lookupSkeleton } from './components/skeletons.js';
import { prefersReducedMotion } from './store/settings.js';
@ -27,8 +25,6 @@ const routes = [
{ pattern: /^#\/types$/, view: () => TypeChartView() },
{ pattern: /^#\/compare$/, view: () => CompareView() },
{ pattern: /^#\/progress$/, view: () => ProgressView() },
{ pattern: /^#\/breeding$/, view: () => BreedingView() },
{ pattern: /^#\/shiny$/, view: () => ShinyView() },
{ pattern: /^#\/search$/, view: () => SearchView() },
{ pattern: /^#\/settings$/, view: () => SettingsView() },
];

View File

@ -1,39 +0,0 @@
import { createStore } from './createStore.js';
/**
* Shiny hunts: one entry per Pokémon you're actively hunting, with an
* encounter counter and the method's odds. Lives in localStorage next to
* the other tracking data; rides along in the JSON export.
*/
export const shinyHunts = createStore('pdx.shinyHunts', { hunts: [] });
let seq = Date.now();
const uid = () => `h${(seq++).toString(36)}`;
export function addHunt({ speciesId, method, charm }) {
shinyHunts.set((s) => ({
...s,
hunts: [
{ id: uid(), speciesId, method, charm: !!charm, count: 0, notes: '', startedAt: new Date().toISOString() },
...s.hunts,
],
}));
}
export function updateHunt(id, patch) {
shinyHunts.set((s) => ({
...s,
hunts: s.hunts.map((h) => (h.id === id ? { ...h, ...patch } : h)),
}));
}
export function bumpHunt(id, delta) {
shinyHunts.set((s) => ({
...s,
hunts: s.hunts.map((h) => (h.id === id ? { ...h, count: Math.max(0, h.count + delta) } : h)),
}));
}
export function removeHunt(id) {
shinyHunts.set((s) => ({ ...s, hunts: s.hunts.filter((h) => h.id !== id) }));
}

View File

@ -1,45 +1,28 @@
import { createStore } from './createStore.js';
/**
* The working "lineup" up to 6 slots analysed by the Team view (coverage
* / compare / calc). A slot is `{ id, form }` where `id` is the national
* dex id and `form` is a snapshot form slug (Mega, regional, alt forme) or
* null for the base Pokémon. Persisted; old saves stored bare ids and are
* migrated on read.
* The working "lineup" up to 6 Pokémon (national dex ids) analysed by the
* Team view (coverage) and Compare view. Persisted.
*/
export const team = createStore('pdx.team', { members: [] });
export const MAX_TEAM = 6;
/** Normalise a stored member (number | {id,form}) to {id, form}. */
export function slot(m) {
return typeof m === 'number' ? { id: m, form: null } : { id: m.id, form: m.form || null };
}
const sameSlot = (a, b) => a.id === b.id && (a.form || null) === (b.form || null);
export function members() {
return team.get().members.map(slot);
}
export function addToTeam(id, form = null) {
export function addToTeam(id) {
team.set((s) => {
const list = s.members.map(slot);
if (list.length >= MAX_TEAM || list.some((m) => sameSlot(m, { id, form }))) return s;
return { ...s, members: [...list, { id, form: form || null }] };
if (s.members.includes(id) || s.members.length >= MAX_TEAM) return s;
return { ...s, members: [...s.members, id] };
});
}
export function removeFromTeam(id, form = null) {
team.set((s) => ({
...s,
members: s.members.map(slot).filter((m) => !sameSlot(m, { id, form: form || null })),
}));
export function removeFromTeam(id) {
team.set((s) => ({ ...s, members: s.members.filter((x) => x !== id) }));
}
export function clearTeam() {
team.set({ members: [] });
}
export function inTeam(id, form = null) {
return team.get().members.map(slot).some((m) => sameSlot(m, { id, form: form || null }));
export function inTeam(id) {
return team.get().members.includes(id);
}

View File

@ -23,9 +23,6 @@ export const ui = createStore('pdx.ui', {
compareIds: [],
toolsOpen: false,
teamMode: 'coverage',
breedMode: 'groups',
breedGroup: '',
breedTarget: 0,
mvType: '',
mvClass: '',
mvSort: 'name',

View File

@ -3099,169 +3099,3 @@
}
.prog__key--caught { background: var(--good); }
.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;
}
/* ---- Shiny hunts ------------------------------------------- */
.shiny {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 14px;
}
.shiny__card {
display: grid;
grid-template-columns: 1fr auto;
grid-template-areas:
'mon counter'
'chance chance'
'notes notes'
'actions actions';
gap: 8px 10px;
padding: 12px;
border-radius: 14px;
background: var(--surface-2);
}
.shiny__mon {
grid-area: mon;
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
color: inherit;
}
.shiny__mon .sprite {
width: 56px;
height: 56px;
object-fit: contain;
}
.shiny__name {
display: block;
font-weight: 700;
text-transform: capitalize;
}
.shiny__method {
display: block;
font-size: 0.74rem;
color: var(--text-dim);
}
.shiny__counter {
grid-area: counter;
display: flex;
align-items: center;
gap: 8px;
}
.shiny__step {
width: 34px;
height: 34px;
border: 1.5px solid var(--border);
border-radius: 50%;
background: var(--surface);
color: var(--text);
font: inherit;
font-weight: 800;
cursor: pointer;
}
.shiny__step--add {
width: 48px;
border-radius: 999px;
background: var(--accent);
border-color: var(--accent);
color: #fff;
}
.shiny__count {
min-width: 3ch;
text-align: center;
font-size: 1.3rem;
font-weight: 800;
font-variant-numeric: tabular-nums;
}
.shiny__chance {
grid-area: chance;
font-size: 0.8rem;
font-weight: 600;
color: var(--text-dim);
}
.shiny__notes {
grid-area: notes;
width: 100%;
padding: 7px 10px;
border: 1px solid var(--border);
border-radius: 10px;
background: var(--surface);
color: var(--text);
font: inherit;
font-size: 0.82rem;
}
.shiny__actions {
grid-area: actions;
display: flex;
gap: 8px;
}
.shiny__actions .button {
flex: 1;
}
.shiny__new {
align-self: flex-start;
}

View File

@ -1,229 +0,0 @@
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;
}

View File

@ -254,23 +254,19 @@ export async function PokemonDetail(nationalId) {
return b;
}
// The active mechanical form (Mega, regional, alt forme) is what gets
// added — its baked types/stats drive the Team coverage/compare/calc.
const teamForm = () => activeForm?.slug || null;
const teamBtn = el('button', {
class: 'ptrack__btn ptrack__btn--team',
type: 'button',
onclick: () => {
if (inTeam(nationalId, teamForm())) removeFromTeam(nationalId, teamForm());
else addToTeam(nationalId, teamForm());
if (inTeam(nationalId)) removeFromTeam(nationalId);
else addToTeam(nationalId);
syncTeamBtn();
},
});
function syncTeamBtn() {
const on = inTeam(nationalId, teamForm());
const on = inTeam(nationalId);
const full = team.get().members.length >= MAX_TEAM;
const label = activeForm ? `${activeForm.name} in team` : 'In team';
teamBtn.textContent = on ? `${label}` : full ? 'Team full' : activeForm ? ` ${activeForm.name}` : ' Team';
teamBtn.textContent = on ? '✓ In team' : full ? 'Team full' : ' Team';
teamBtn.classList.toggle('is-on', on);
teamBtn.disabled = !on && full;
}
@ -666,7 +662,6 @@ export async function PokemonDetail(nationalId) {
activeCosmetic = null;
if (cosmeticSelect) cosmeticSelect.value = '';
syncFormPills();
syncTeamBtn();
heroForm.textContent = form ? form.name : '';
try {
pk = form ? await getPokemon(form.slug) : pokemon;

View File

@ -1,9 +1,6 @@
import { el, onTeardown } from '../lib/dom.js';
import { settings, applyTheme } from '../store/settings.js';
import { selection } from '../store/selection.js';
import { team } from '../store/team.js';
import { formTracking } from '../store/formTracking.js';
import { shinyHunts } from '../store/shinyHunts.js';
import { canInstall, onInstallChange, promptInstall } from '../lib/install.js';
export async function SettingsView() {
@ -133,9 +130,6 @@ export async function SettingsView() {
const data = JSON.parse(await file.text());
if (data.settings) settings.replace(data.settings);
if (data.selection) selection.replace(data.selection);
if (data.team) team.replace(data.team);
if (data.formTracking) formTracking.replace(data.formTracking);
if (data.shinyHunts) shinyHunts.replace(data.shinyHunts);
alert('Import complete.');
} catch {
alert('That file could not be read as a Pokédex backup.');
@ -192,8 +186,7 @@ export async function SettingsView() {
'p',
{ class: 'settings__note' },
el('a', { class: 'link', href: '#/progress' }, 'Progress by game'),
' · ',
el('a', { class: 'link', href: '#/shiny' }, 'Shiny hunts'),
' — caught totals against every games dex.',
),
storageNote,
el('div', { class: 'settings__actions' },
@ -260,9 +253,6 @@ function exportData() {
exportedAt: new Date().toISOString(),
settings: settings.get(),
selection: selection.get(),
team: team.get(),
formTracking: formTracking.get(),
shinyHunts: shinyHunts.get(),
};
const blob = new Blob([JSON.stringify(payload, null, 2)], {
type: 'application/json',

View File

@ -1,194 +0,0 @@
import { el, clear, onTeardown } from '../lib/dom.js';
import { loadSnapshot } from '../data/snapshot.js';
import { settings } from '../store/settings.js';
import { shinyHunts, addHunt, updateHunt, bumpHunt, removeHunt } from '../store/shinyHunts.js';
import { toggle, entry } from '../store/selection.js';
import { buzz } from '../lib/haptics.js';
import { Sprite } from '../components/Sprite.js';
import { openPokemonPicker } from '../components/PokemonPicker.js';
// Representative full-game odds (as 1/N), with a shiny-charm variant.
export const METHODS = [
{ id: 'full', label: 'Full odds', base: 4096, charm: 1365 },
{ id: 'masuda', label: 'Masuda Method', base: 683, charm: 512 },
{ id: 'sos', label: 'SOS chaining (31+)', base: 1365, charm: 1024 },
{ id: 'radar', label: 'Poké Radar (chain 40)', base: 99, charm: 99 },
{ id: 'dexnav', label: 'DexNav (high search level)', base: 1365, charm: 1024 },
{ id: 'combo', label: 'Catch Combo (31+)', base: 1024, charm: 819 },
{ id: 'dmax', label: 'Dynamax Adventure', base: 300, charm: 100 },
{ id: 'outbreak', label: 'Mass Outbreak (SV, 60+)', base: 512, charm: 205 },
{ id: 'massive', label: 'Massive Outbreak (Legends)', base: 158, charm: 128 },
];
const methodById = (id) => METHODS.find((m) => m.id === id) || METHODS[0];
function oddsFor(h) {
if (h.customOdds) return Math.max(1, h.customOdds);
const m = methodById(h.method);
return h.charm ? m.charm : m.base;
}
const pct = (n) => (n < 0.1 ? n.toFixed(2) : n < 10 ? n.toFixed(1) : Math.round(n));
export async function ShinyView() {
const view = el('section', { class: 'view lookup shinyview' });
const snap = await loadSnapshot();
const style = settings.get().spriteStyle;
const list = el('div', { class: 'shiny' });
function huntCard(h) {
const sp = snap.speciesById.get(h.speciesId);
const odds = oddsFor(h);
const cumulative = (n) => 1 - Math.pow(1 - 1 / odds, n);
const count = el('span', { class: 'shiny__count' }, String(h.count));
const chanceEl = el(
'span',
{ class: 'shiny__chance' },
`${pct(cumulative(h.count) * 100)}% by now · 1/${odds}`,
);
const refresh = () => {
const n = shinyHunts.get().hunts.find((x) => x.id === h.id)?.count ?? 0;
count.textContent = String(n);
chanceEl.textContent = `${pct(cumulative(n) * 100)}% by now · 1/${odds}`;
};
const notes = el('input', {
type: 'text',
class: 'shiny__notes',
value: h.notes || '',
placeholder: 'Notes — location, den, phase count…',
onchange: (e) => updateHunt(h.id, { notes: e.target.value }),
});
return el(
'div',
{ class: 'shiny__card' },
el(
'a',
{ class: 'shiny__mon', href: `#/pokemon/${h.speciesId}` },
Sprite(h.speciesId, { style, shiny: true, alt: sp?.name, size: 56 }),
el(
'span',
{},
el('span', { class: 'shiny__name' }, (sp?.name || `#${h.speciesId}`).replace(/-/g, ' ')),
el('span', { class: 'shiny__method' }, methodById(h.method).label + (h.charm ? ' · charm' : '')),
),
),
el(
'div',
{ class: 'shiny__counter' },
el('button', { type: 'button', class: 'shiny__step', onclick: () => { bumpHunt(h.id, -1); refresh(); } }, ''),
count,
el(
'button',
{
type: 'button',
class: 'shiny__step shiny__step--add',
onclick: () => {
bumpHunt(h.id, 1);
buzz(10);
refresh();
},
},
'+1',
),
),
chanceEl,
notes,
el(
'div',
{ class: 'shiny__actions' },
el(
'button',
{
type: 'button',
class: 'button',
onclick: () => {
if (!entry(h.speciesId).caught) toggle(h.speciesId, 'caught');
removeHunt(h.id);
render();
},
},
'✨ Found it',
),
el(
'button',
{
type: 'button',
class: 'button button--danger',
onclick: () => {
if (confirm('Delete this hunt?')) {
removeHunt(h.id);
render();
}
},
},
'Delete',
),
),
);
}
function newHunt() {
openPokemonPicker(
(id) => chooseMethod((method, charm) => addHunt({ speciesId: id, method, charm })),
{ closeAfterPick: true },
);
}
function chooseMethod(done) {
const backdrop = el('div', { class: 'sheet-backdrop is-open', onclick: (e) => e.target === backdrop && backdrop.remove() });
const methodSel = el('select', { class: 'search__input' }, ...METHODS.map((m) => el('option', { value: m.id }, m.label)));
const charmChk = el('input', { type: 'checkbox' });
const panel = el(
'div',
{ class: 'gsheet' },
el('div', { class: 'gsheet__head' }, el('h2', {}, 'Hunt method'), el('button', { class: 'gsheet__close', onclick: () => backdrop.remove() }, '✕')),
methodSel,
el('label', { class: 'lookup__check', style: 'margin-top:12px' }, charmChk, 'Shiny Charm'),
el(
'button',
{
class: 'button',
style: 'margin-top:16px',
onclick: () => {
done(methodSel.value, charmChk.checked);
backdrop.remove();
render();
},
},
'Start hunt',
),
);
backdrop.append(panel);
document.body.append(backdrop);
}
function render() {
clear(list);
const hunts = shinyHunts.get().hunts;
if (!hunts.length) {
list.append(el('p', { class: 'detail__muted' }, 'No active hunts. Start one below.'));
} else {
for (const h of hunts) list.append(huntCard(h));
}
list.append(
el('button', { type: 'button', class: 'lineup__add shiny__new', onclick: newHunt }, '', el('span', {}, 'New hunt')),
);
}
clear(view).append(
el('nav', { class: 'lookup__nav' }, el('a', { class: 'link', href: '#/settings' }, ' Settings')),
el(
'header',
{ class: 'view__header' },
el('h1', {}, 'Shiny hunts'),
el('p', {}, 'Count encounters and watch the cumulative odds climb. “Found it” marks the Pokémon caught.'),
),
list,
);
render();
onTeardown(view, () => {});
return view;
}

View File

@ -2,7 +2,7 @@ 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 { team, addToTeam, removeFromTeam, clearTeam, slot, MAX_TEAM } from '../store/team.js';
import { team, addToTeam, removeFromTeam, clearTeam, MAX_TEAM } from '../store/team.js';
import { TYPES, defenseVector, multiplier } from '../data/type-chart.js';
import { typesForGen } from '../lib/type-resolve.js';
import { forGeneration } from '../components/MovesList.js';
@ -70,8 +70,6 @@ export async function TeamView() {
el('a', { class: 'link', href: '#/types' }, 'Type chart'),
' · ',
el('a', { class: 'link', href: '#/compare' }, 'Compare'),
' · ',
el('a', { class: 'link', href: '#/breeding' }, 'Breeding'),
),
),
seg,
@ -80,28 +78,7 @@ export async function TeamView() {
);
function members() {
return team
.get()
.members.map((m) => {
const s = slot(m);
const base = snap.speciesById.get(s.id);
if (!base) return null;
const f = s.form && (base.forms || []).find((x) => x.slug === s.form);
if (!f) return { ...base, _slotForm: null };
// Merge the snapshot form's baked types / stats so coverage,
// compare and the calc all analyse the form, not the base.
return {
...base,
_slotForm: s.form,
name: `${base.name} (${f.name})`,
spriteId: f.id,
types: f.types || base.types,
pastTypes: [],
stats: f.stats || base.stats,
bst: f.bst ?? base.bst,
};
})
.filter(Boolean);
return team.get().members.map((id) => snap.speciesById.get(id)).filter(Boolean);
}
function renderLineup() {
@ -120,7 +97,7 @@ export async function TeamView() {
type: 'button',
'aria-label': `Remove ${sp.name}`,
onclick: () => {
removeFromTeam(sp.id, sp._slotForm);
removeFromTeam(sp.id);
},
},
'✕',
@ -128,7 +105,7 @@ export async function TeamView() {
el(
'a',
{ href: `#/pokemon/${sp.id}`, class: 'lineup__link' },
Sprite(sp.spriteId ?? sp.id, { style, alt: sp.name, size: 64 }),
Sprite(sp.id, { style, alt: sp.name, size: 64 }),
el('span', { class: 'lineup__name' }, sp.name.replace(/-/g, ' ')),
),
),
@ -141,7 +118,7 @@ export async function TeamView() {
{
class: 'lineup__add',
type: 'button',
onclick: () => openPokemonPicker((id) => addToTeam(id), { closeAfterPick: true }),
onclick: () => openPokemonPicker(addToTeam),
},
'',
el('span', {}, 'Add'),
@ -236,7 +213,7 @@ export async function TeamView() {
href: `#/pokemon/${sp.id}`,
title: `${prettify(sp.name)} takes ${m}×`,
},
Sprite(sp.spriteId ?? sp.id, { style, size: 26, alt: sp.name }),
Sprite(sp.id, { style, size: 26, alt: sp.name }),
),
),
),
@ -255,7 +232,7 @@ export async function TeamView() {
el(
'a',
{ class: 'cov__mem', href: `#/pokemon/${sp.id}`, title: prettify(sp.name) },
Sprite(sp.spriteId ?? sp.id, { style, size: 34, alt: sp.name }),
Sprite(sp.id, { style, size: 34, alt: sp.name }),
),
),
el('span', { class: 'cov__bal cov__bal--head' }, 'resist · weak'),
@ -431,7 +408,7 @@ export async function TeamView() {
? el(
'button',
{ type: 'button', class: 'calc__pick', onclick: () => pick() },
Sprite(sp.spriteId ?? sp.id, { style, size: 48, alt: sp.name }),
Sprite(sp.id, { style, size: 48, alt: sp.name }),
el('span', {}, prettify(sp.name)),
)
: el('button', { type: 'button', class: 'calc__pick calc__pick--empty', onclick: () => pick() }, `Choose ${sideLabel}`),
@ -462,7 +439,7 @@ export async function TeamView() {
recalc();
},
},
Sprite(sp.spriteId ?? sp.id, { style, size: 26, alt: sp.name }),
Sprite(sp.id, { style, size: 26, alt: sp.name }),
),
),
)