Compare commits
3 Commits
53f4f4381d
...
7f12f08718
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f12f08718 | |||
| b15ee3dd7f | |||
| 48cb471489 |
@ -18,7 +18,8 @@ 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).
|
- **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.
|
- **Sprites** → cache-first with a capped LRU.
|
||||||
|
|
||||||
Export / import of `settings` + `selection` as JSON lives in **Settings**.
|
Export / import of all local state (settings, tracking, team, form
|
||||||
|
tracking, shiny hunts) as JSON lives in **Settings**.
|
||||||
|
|
||||||
## Scripts
|
## Scripts
|
||||||
|
|
||||||
|
|||||||
@ -31,7 +31,7 @@ export function CompareTable(members, { gen = 9, style } = {}) {
|
|||||||
el(
|
el(
|
||||||
'a',
|
'a',
|
||||||
{ class: 'cmp__mem', href: `#/pokemon/${sp.id}` },
|
{ class: 'cmp__mem', href: `#/pokemon/${sp.id}` },
|
||||||
Sprite(sp.id, { style, size: 44, alt: sp.name }),
|
Sprite(sp.spriteId ?? sp.id, { style, size: 44, alt: sp.name }),
|
||||||
el('span', {}, sp.name.replace(/-/g, ' ')),
|
el('span', {}, sp.name.replace(/-/g, ' ')),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -11,7 +11,7 @@ let openInstance = null;
|
|||||||
* `onPick(id)` fires per selection; the sheet stays open so you can add
|
* `onPick(id)` fires per selection; the sheet stays open so you can add
|
||||||
* several. Dismiss with the backdrop, ✕, or Escape.
|
* several. Dismiss with the backdrop, ✕, or Escape.
|
||||||
*/
|
*/
|
||||||
export async function openPokemonPicker(onPick) {
|
export async function openPokemonPicker(onPick, { closeAfterPick = false } = {}) {
|
||||||
if (openInstance) return;
|
if (openInstance) return;
|
||||||
const snap = await loadSnapshot();
|
const snap = await loadSnapshot();
|
||||||
|
|
||||||
@ -69,7 +69,10 @@ export async function openPokemonPicker(onPick) {
|
|||||||
{
|
{
|
||||||
class: 'picker__row',
|
class: 'picker__row',
|
||||||
type: 'button',
|
type: 'button',
|
||||||
onclick: () => onPick(s.id),
|
onclick: () => {
|
||||||
|
onPick(s.id);
|
||||||
|
if (closeAfterPick) close();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Sprite(s.id, { style, alt: s.name, size: 44 }),
|
Sprite(s.id, { style, alt: s.name, size: 44 }),
|
||||||
el('span', { class: 'search__num' }, `#${String(s.id).padStart(4, '0')}`),
|
el('span', { class: 'search__num' }, `#${String(s.id).padStart(4, '0')}`),
|
||||||
|
|||||||
@ -11,6 +11,8 @@ 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 { ShinyView } from './views/ShinyView.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 +27,8 @@ 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: /^#\/shiny$/, view: () => ShinyView() },
|
||||||
{ pattern: /^#\/search$/, view: () => SearchView() },
|
{ pattern: /^#\/search$/, view: () => SearchView() },
|
||||||
{ pattern: /^#\/settings$/, view: () => SettingsView() },
|
{ pattern: /^#\/settings$/, view: () => SettingsView() },
|
||||||
];
|
];
|
||||||
|
|||||||
39
src/store/shinyHunts.js
Normal file
39
src/store/shinyHunts.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
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) }));
|
||||||
|
}
|
||||||
@ -1,28 +1,45 @@
|
|||||||
import { createStore } from './createStore.js';
|
import { createStore } from './createStore.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The working "lineup" — up to 6 Pokémon (national dex ids) analysed by the
|
* The working "lineup" — up to 6 slots analysed by the Team view (coverage
|
||||||
* Team view (coverage) and Compare view. Persisted.
|
* / 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.
|
||||||
*/
|
*/
|
||||||
export const team = createStore('pdx.team', { members: [] });
|
export const team = createStore('pdx.team', { members: [] });
|
||||||
|
|
||||||
export const MAX_TEAM = 6;
|
export const MAX_TEAM = 6;
|
||||||
|
|
||||||
export function addToTeam(id) {
|
/** 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) {
|
||||||
team.set((s) => {
|
team.set((s) => {
|
||||||
if (s.members.includes(id) || s.members.length >= MAX_TEAM) return s;
|
const list = s.members.map(slot);
|
||||||
return { ...s, members: [...s.members, id] };
|
if (list.length >= MAX_TEAM || list.some((m) => sameSlot(m, { id, form }))) return s;
|
||||||
|
return { ...s, members: [...list, { id, form: form || null }] };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeFromTeam(id) {
|
export function removeFromTeam(id, form = null) {
|
||||||
team.set((s) => ({ ...s, members: s.members.filter((x) => x !== id) }));
|
team.set((s) => ({
|
||||||
|
...s,
|
||||||
|
members: s.members.map(slot).filter((m) => !sameSlot(m, { id, form: form || null })),
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearTeam() {
|
export function clearTeam() {
|
||||||
team.set({ members: [] });
|
team.set({ members: [] });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function inTeam(id) {
|
export function inTeam(id, form = null) {
|
||||||
return team.get().members.includes(id);
|
return team.get().members.map(slot).some((m) => sameSlot(m, { id, form: form || null }));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,169 @@
|
|||||||
}
|
}
|
||||||
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 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;
|
||||||
|
}
|
||||||
|
|||||||
229
src/views/BreedingView.js
Normal file
229
src/views/BreedingView.js
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@ -254,19 +254,23 @@ export async function PokemonDetail(nationalId) {
|
|||||||
return b;
|
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', {
|
const teamBtn = el('button', {
|
||||||
class: 'ptrack__btn ptrack__btn--team',
|
class: 'ptrack__btn ptrack__btn--team',
|
||||||
type: 'button',
|
type: 'button',
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
if (inTeam(nationalId)) removeFromTeam(nationalId);
|
if (inTeam(nationalId, teamForm())) removeFromTeam(nationalId, teamForm());
|
||||||
else addToTeam(nationalId);
|
else addToTeam(nationalId, teamForm());
|
||||||
syncTeamBtn();
|
syncTeamBtn();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
function syncTeamBtn() {
|
function syncTeamBtn() {
|
||||||
const on = inTeam(nationalId);
|
const on = inTeam(nationalId, teamForm());
|
||||||
const full = team.get().members.length >= MAX_TEAM;
|
const full = team.get().members.length >= MAX_TEAM;
|
||||||
teamBtn.textContent = on ? '✓ In team' : full ? 'Team full' : '+ Team';
|
const label = activeForm ? `${activeForm.name} in team` : 'In team';
|
||||||
|
teamBtn.textContent = on ? `✓ ${label}` : full ? 'Team full' : activeForm ? `+ ${activeForm.name}` : '+ Team';
|
||||||
teamBtn.classList.toggle('is-on', on);
|
teamBtn.classList.toggle('is-on', on);
|
||||||
teamBtn.disabled = !on && full;
|
teamBtn.disabled = !on && full;
|
||||||
}
|
}
|
||||||
@ -662,6 +666,7 @@ export async function PokemonDetail(nationalId) {
|
|||||||
activeCosmetic = null;
|
activeCosmetic = null;
|
||||||
if (cosmeticSelect) cosmeticSelect.value = '';
|
if (cosmeticSelect) cosmeticSelect.value = '';
|
||||||
syncFormPills();
|
syncFormPills();
|
||||||
|
syncTeamBtn();
|
||||||
heroForm.textContent = form ? form.name : '';
|
heroForm.textContent = form ? form.name : '';
|
||||||
try {
|
try {
|
||||||
pk = form ? await getPokemon(form.slug) : pokemon;
|
pk = form ? await getPokemon(form.slug) : pokemon;
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import { el, onTeardown } from '../lib/dom.js';
|
import { el, onTeardown } from '../lib/dom.js';
|
||||||
import { settings, applyTheme } from '../store/settings.js';
|
import { settings, applyTheme } from '../store/settings.js';
|
||||||
import { selection } from '../store/selection.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';
|
import { canInstall, onInstallChange, promptInstall } from '../lib/install.js';
|
||||||
|
|
||||||
export async function SettingsView() {
|
export async function SettingsView() {
|
||||||
@ -130,6 +133,9 @@ export async function SettingsView() {
|
|||||||
const data = JSON.parse(await file.text());
|
const data = JSON.parse(await file.text());
|
||||||
if (data.settings) settings.replace(data.settings);
|
if (data.settings) settings.replace(data.settings);
|
||||||
if (data.selection) selection.replace(data.selection);
|
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.');
|
alert('Import complete.');
|
||||||
} catch {
|
} catch {
|
||||||
alert('That file could not be read as a Pokédex backup.');
|
alert('That file could not be read as a Pokédex backup.');
|
||||||
@ -186,7 +192,8 @@ export async function SettingsView() {
|
|||||||
'p',
|
'p',
|
||||||
{ class: 'settings__note' },
|
{ class: 'settings__note' },
|
||||||
el('a', { class: 'link', href: '#/progress' }, 'Progress by game'),
|
el('a', { class: 'link', href: '#/progress' }, 'Progress by game'),
|
||||||
' — caught totals against every game’s dex.',
|
' · ',
|
||||||
|
el('a', { class: 'link', href: '#/shiny' }, 'Shiny hunts'),
|
||||||
),
|
),
|
||||||
storageNote,
|
storageNote,
|
||||||
el('div', { class: 'settings__actions' },
|
el('div', { class: 'settings__actions' },
|
||||||
@ -253,6 +260,9 @@ function exportData() {
|
|||||||
exportedAt: new Date().toISOString(),
|
exportedAt: new Date().toISOString(),
|
||||||
settings: settings.get(),
|
settings: settings.get(),
|
||||||
selection: selection.get(),
|
selection: selection.get(),
|
||||||
|
team: team.get(),
|
||||||
|
formTracking: formTracking.get(),
|
||||||
|
shinyHunts: shinyHunts.get(),
|
||||||
};
|
};
|
||||||
const blob = new Blob([JSON.stringify(payload, null, 2)], {
|
const blob = new Blob([JSON.stringify(payload, null, 2)], {
|
||||||
type: 'application/json',
|
type: 'application/json',
|
||||||
|
|||||||
194
src/views/ShinyView.js
Normal file
194
src/views/ShinyView.js
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@ import { el, clear, onTeardown } from '../lib/dom.js';
|
|||||||
import { loadSnapshot } from '../data/snapshot.js';
|
import { loadSnapshot } from '../data/snapshot.js';
|
||||||
import { settings } from '../store/settings.js';
|
import { settings } from '../store/settings.js';
|
||||||
import { ui } from '../store/ui.js';
|
import { ui } from '../store/ui.js';
|
||||||
import { team, addToTeam, removeFromTeam, clearTeam, MAX_TEAM } from '../store/team.js';
|
import { team, addToTeam, removeFromTeam, clearTeam, slot, MAX_TEAM } from '../store/team.js';
|
||||||
import { TYPES, defenseVector, multiplier } from '../data/type-chart.js';
|
import { TYPES, defenseVector, multiplier } from '../data/type-chart.js';
|
||||||
import { typesForGen } from '../lib/type-resolve.js';
|
import { typesForGen } from '../lib/type-resolve.js';
|
||||||
import { forGeneration } from '../components/MovesList.js';
|
import { forGeneration } from '../components/MovesList.js';
|
||||||
@ -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,
|
||||||
@ -78,7 +80,28 @@ export async function TeamView() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
function members() {
|
function members() {
|
||||||
return team.get().members.map((id) => snap.speciesById.get(id)).filter(Boolean);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderLineup() {
|
function renderLineup() {
|
||||||
@ -97,7 +120,7 @@ export async function TeamView() {
|
|||||||
type: 'button',
|
type: 'button',
|
||||||
'aria-label': `Remove ${sp.name}`,
|
'aria-label': `Remove ${sp.name}`,
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
removeFromTeam(sp.id);
|
removeFromTeam(sp.id, sp._slotForm);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'✕',
|
'✕',
|
||||||
@ -105,7 +128,7 @@ export async function TeamView() {
|
|||||||
el(
|
el(
|
||||||
'a',
|
'a',
|
||||||
{ href: `#/pokemon/${sp.id}`, class: 'lineup__link' },
|
{ href: `#/pokemon/${sp.id}`, class: 'lineup__link' },
|
||||||
Sprite(sp.id, { style, alt: sp.name, size: 64 }),
|
Sprite(sp.spriteId ?? sp.id, { style, alt: sp.name, size: 64 }),
|
||||||
el('span', { class: 'lineup__name' }, sp.name.replace(/-/g, ' ')),
|
el('span', { class: 'lineup__name' }, sp.name.replace(/-/g, ' ')),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -118,7 +141,7 @@ export async function TeamView() {
|
|||||||
{
|
{
|
||||||
class: 'lineup__add',
|
class: 'lineup__add',
|
||||||
type: 'button',
|
type: 'button',
|
||||||
onclick: () => openPokemonPicker(addToTeam),
|
onclick: () => openPokemonPicker((id) => addToTeam(id), { closeAfterPick: true }),
|
||||||
},
|
},
|
||||||
'+',
|
'+',
|
||||||
el('span', {}, 'Add'),
|
el('span', {}, 'Add'),
|
||||||
@ -213,7 +236,7 @@ export async function TeamView() {
|
|||||||
href: `#/pokemon/${sp.id}`,
|
href: `#/pokemon/${sp.id}`,
|
||||||
title: `${prettify(sp.name)} takes ${m}×`,
|
title: `${prettify(sp.name)} takes ${m}×`,
|
||||||
},
|
},
|
||||||
Sprite(sp.id, { style, size: 26, alt: sp.name }),
|
Sprite(sp.spriteId ?? sp.id, { style, size: 26, alt: sp.name }),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -232,7 +255,7 @@ export async function TeamView() {
|
|||||||
el(
|
el(
|
||||||
'a',
|
'a',
|
||||||
{ class: 'cov__mem', href: `#/pokemon/${sp.id}`, title: prettify(sp.name) },
|
{ class: 'cov__mem', href: `#/pokemon/${sp.id}`, title: prettify(sp.name) },
|
||||||
Sprite(sp.id, { style, size: 34, alt: sp.name }),
|
Sprite(sp.spriteId ?? sp.id, { style, size: 34, alt: sp.name }),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
el('span', { class: 'cov__bal cov__bal--head' }, 'resist · weak'),
|
el('span', { class: 'cov__bal cov__bal--head' }, 'resist · weak'),
|
||||||
@ -408,7 +431,7 @@ export async function TeamView() {
|
|||||||
? el(
|
? el(
|
||||||
'button',
|
'button',
|
||||||
{ type: 'button', class: 'calc__pick', onclick: () => pick() },
|
{ type: 'button', class: 'calc__pick', onclick: () => pick() },
|
||||||
Sprite(sp.id, { style, size: 48, alt: sp.name }),
|
Sprite(sp.spriteId ?? sp.id, { style, size: 48, alt: sp.name }),
|
||||||
el('span', {}, prettify(sp.name)),
|
el('span', {}, prettify(sp.name)),
|
||||||
)
|
)
|
||||||
: el('button', { type: 'button', class: 'calc__pick calc__pick--empty', onclick: () => pick() }, `Choose ${sideLabel}…`),
|
: el('button', { type: 'button', class: 'calc__pick calc__pick--empty', onclick: () => pick() }, `Choose ${sideLabel}…`),
|
||||||
@ -439,7 +462,7 @@ export async function TeamView() {
|
|||||||
recalc();
|
recalc();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Sprite(sp.id, { style, size: 26, alt: sp.name }),
|
Sprite(sp.spriteId ?? sp.id, { style, size: 26, alt: sp.name }),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user