diff --git a/README.md b/README.md index 0bccae9..779a83f 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,10 @@ Working app with a type-themed UI: filter drawer: type, generation, ability, egg group, minimum BST, and "fully evolved only" (derived client-side from each species' evolves-from link — no extra fetches). A "Notes" chip filters to Pokémon with a note; - cards with one get a small 📝 mark. + cards with one get a small 📝 mark. When a specific game is selected, + cards show a version-exclusive badge ("Ruby only") — derived from the + snapshot's baked wild-encounter data, so gift/trade-only exclusives are + missed. - **Notes** — a free-text note per Pokémon on its detail page (trade plans, where you caught it, anything), autosaving as you type and flushing immediately on blur or navigation so a quick tab-away never drops a @@ -135,8 +138,8 @@ Working app with a type-themed UI: offer (toast + Settings button), manifest shortcuts to Team / Search / Type chart. -Not yet done: version-exclusive badges, an interactive region map (PokéAPI -has no map imagery or coordinates), and a formal Lighthouse pass. +Not yet done: an interactive region map (PokéAPI has no map imagery or +coordinates), and a formal Lighthouse pass. ## Notes diff --git a/scripts/build-snapshot.mjs b/scripts/build-snapshot.mjs index 8ac3179..1f69e0e 100644 --- a/scripts/build-snapshot.mjs +++ b/scripts/build-snapshot.mjs @@ -320,6 +320,35 @@ async function main() { }; }); + // ---- Version exclusivity (from wild encounters) --------------- + // For each game, the versions a species has wild encounters in. If that's + // a non-empty strict subset of the game's versions, it's version + // exclusive there. Wild-only — gift/trade/evolution exclusives are missed. + const versionToVg = {}; + for (const vg of versionGroups) for (const v of vg.versions) versionToVg[v] = vg.key; + const vgVersionCount = Object.fromEntries(versionGroups.map((vg) => [vg.key, vg.versions.length])); + console.log(`Fetching wild encounters for ${species.length} species …`); + await mapLimit(species, CONCURRENCY, async (sp) => { + let enc; + try { + enc = await api(`pokemon/${sp.id}/encounters`); + } catch { + return; + } + const byVg = {}; + for (const area of enc) { + for (const vd of area.version_details || []) { + const vg = versionToVg[vd.version.name]; + if (vg) (byVg[vg] ||= new Set()).add(vd.version.name); + } + } + const excl = {}; + for (const [vg, set] of Object.entries(byVg)) { + if (set.size > 0 && set.size < (vgVersionCount[vg] || 1)) excl[vg] = [...set]; + } + if (Object.keys(excl).length) sp.exclusiveIn = excl; + }); + // ---- Moves index ------------------------------------------- const moveIndex = await api('move?limit=100000'); console.log(`Fetching ${moveIndex.results.length} moves …`); diff --git a/src/components/Card.js b/src/components/Card.js index bd90d3a..8066813 100644 --- a/src/components/Card.js +++ b/src/components/Card.js @@ -6,6 +6,7 @@ import { typeHex } from '../lib/type-color.js'; import { typesForGen } from '../lib/type-resolve.js'; import { prefersReducedMotion } from '../store/settings.js'; import { buzz } from '../lib/haptics.js'; +import { prettify } from '../data/pokedex-resolver.js'; /** * One Pokémon in the dex feed. Tinted by its primary type: a soft top-down @@ -73,6 +74,12 @@ export function Card(species, number, { spriteStyle = 'official', versionGroup, const n = (species.forms?.length || 0) + (species.cosmeticForms?.length || 0); return n ? el('span', { class: 'card__forms' }, `+${n} form${n > 1 ? 's' : ''}`) : null; })(), + (() => { + const ex = versionGroup && versionGroup !== 'all' && species.exclusiveIn?.[versionGroup]; + return ex + ? el('span', { class: 'card__excl', title: 'Version exclusive (wild)' }, `${ex.map(prettify).join(' & ')} only`) + : null; + })(), metric ? el('span', { class: 'card__metric' }, metric) : null, ), ); diff --git a/src/styles/layout.css b/src/styles/layout.css index c6ac1c3..bc9afe1 100644 --- a/src/styles/layout.css +++ b/src/styles/layout.css @@ -2912,6 +2912,18 @@ font-weight: 700; color: color-mix(in srgb, var(--type-main) 55%, var(--text-dim)); } +.card__excl { + align-self: flex-start; + margin-top: 4px; + font-size: 0.6rem; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.03em; + padding: 2px 7px; + border-radius: 999px; + background: color-mix(in srgb, var(--type-main) 22%, var(--surface-2)); + color: color-mix(in srgb, var(--type-main) 70%, var(--ink)); +} /* ---- Type chart page ---------------------------------------- */ .tchart__scroll {