Version-exclusive badges on dex cards

build-snapshot: a new pass fetches /pokemon/:id/encounters for every
species and records, per game, the versions it has wild encounters in —
when that's a non-empty strict subset of the game's versions,
species.exclusiveIn[vg] holds them. Wild-only, so gift/trade/evolution
exclusives are missed.

Card: with a specific game selected, shows a "<Version> only" pill
(e.g. Seedot -> "Ruby only", Lotad -> "Sapphire only"). 220 species
flagged across all games; nothing shown in National mode.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
This commit is contained in:
chris 2026-09-09 19:26:56 -04:00
parent 7f12f08718
commit 2e4d9ab329
4 changed files with 54 additions and 3 deletions

View File

@ -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

View File

@ -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 …`);

View File

@ -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,
),
);

View File

@ -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 {