Trim typings and matchups to the selected generation

- Detail page resolves a Pokemon's typing from PokeAPI past_types for the
  selected game's generation (Gen 1 Magnemite is pure Electric, pre-Gen 6
  Clefairy is Normal); feeds hero pills, theming and matchups
- Type chart excludes Steel/Dark (pre-Gen 2) and Fairy (pre-Gen 6) as
  attacking types entirely
- Gen 1-2 game-era sprites (opaque white bg) shown as a small framed tile
  on cards and the detail hero

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
chris 2026-08-27 12:27:02 -04:00
parent 9216a53932
commit 404ca7dbbd
5 changed files with 57 additions and 8 deletions

View File

@ -9,10 +9,12 @@ import { typeHex } from '../lib/type-color.js';
* gradient, a type-coloured spotlight behind an oversized sprite that lifts * gradient, a type-coloured spotlight behind an oversized sprite that lifts
* above the card, a number chip, and a big ghost number in the corner. * above the card, a number chip, and a big ghost number in the corner.
*/ */
export function Card(species, number, { spriteStyle = 'official', versionGroup } = {}) { export function Card(species, number, { spriteStyle = 'official', versionGroup, boxed = false } = {}) {
const state = entry(species.id); const state = entry(species.id);
const mainType = (species.types || [])[0] || 'normal'; const mainType = (species.types || [])[0] || 'normal';
const num = String(number ?? species.id).padStart(3, '0'); const num = String(number ?? species.id).padStart(3, '0');
const data = { type: mainType, sprite: spriteStyle };
if (boxed) data.boxed = '1';
const caughtBtn = el('button', { const caughtBtn = el('button', {
class: 'card__caught', class: 'card__caught',
@ -34,7 +36,7 @@ export function Card(species, number, { spriteStyle = 'official', versionGroup }
{ {
class: `card${state.caught ? ' is-caught' : ''}`, class: `card${state.caught ? ' is-caught' : ''}`,
href: `#/pokemon/${species.id}`, href: `#/pokemon/${species.id}`,
dataset: { type: mainType, sprite: spriteStyle }, dataset: data,
style: `--type-main:${typeHex(mainType)}`, style: `--type-main:${typeHex(mainType)}`,
}, },
el('span', { class: 'card__ghost', 'aria-hidden': 'true' }, num), el('span', { class: 'card__ghost', 'aria-hidden': 'true' }, num),

View File

@ -70,7 +70,10 @@ export function multiplier(attacking, defTypes, gen = 9) {
*/ */
export function defensiveMatchups(defTypes, gen = 9) { export function defensiveMatchups(defTypes, gen = 9) {
const buckets = { '4': [], '2': [], '0.5': [], '0.25': [], '0': [] }; const buckets = { '4': [], '2': [], '0.5': [], '0.25': [], '0': [] };
const atkTypes = gen < 6 ? TYPES.filter((t) => t !== 'fairy') : TYPES; // Types that didn't exist yet can't be attacking: Steel/Dark before Gen 2,
// Fairy before Gen 6.
const absent = new Set(gen < 2 ? ['steel', 'dark', 'fairy'] : gen < 6 ? ['fairy'] : []);
const atkTypes = TYPES.filter((t) => !absent.has(t));
for (const atk of atkTypes) { for (const atk of atkTypes) {
const m = multiplier(atk, defTypes, gen); const m = multiplier(atk, defTypes, gen);
if (m === 1) continue; if (m === 1) continue;

View File

@ -450,6 +450,23 @@
.card[data-sprite="game"] .card__art .sprite { .card[data-sprite="game"] .card__art .sprite {
image-rendering: pixelated; image-rendering: pixelated;
} }
.card[data-sprite="game"] .card__art {
width: 96px;
height: 96px;
}
/* Gen 12 game sprites ship with an opaque white background show them as
a small framed tile rather than a box that swallows the card. */
.card[data-boxed] .card__art {
width: 78px;
height: 78px;
padding: 5px;
border-radius: 12px;
background: #f7f7f5;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08), 0 2px 6px rgba(0, 0, 0, 0.14);
}
.card[data-boxed] .card__art .sprite {
filter: none;
}
.card__body { .card__body {
position: relative; position: relative;
z-index: 1; z-index: 1;
@ -703,6 +720,18 @@
height: 100%; height: 100%;
object-fit: contain; object-fit: contain;
} }
.phero__art--boxed {
width: min(180px, 46vw);
height: min(180px, 46vw);
padding: 14px;
border-radius: 18px;
background: #f7f7f5;
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.22), inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.phero__art--boxed img {
image-rendering: pixelated;
filter: none;
}
@keyframes pop { @keyframes pop {
from { transform: scale(0.8); opacity: 0; } from { transform: scale(0.8); opacity: 0; }
to { transform: scale(1); opacity: 1; } to { transform: scale(1); opacity: 1; }

View File

@ -113,6 +113,10 @@ export async function DexGrid() {
function paintGrid() { function paintGrid() {
const st = settings.get(); const st = settings.get();
const pokemonState = selection.get().pokemon; const pokemonState = selection.get().pokemon;
// Gen 12 game-era sprites have opaque white backgrounds — frame them.
const boxed =
st.spriteStyle === 'game' &&
(snap.versionGroupByKey.get(st.versionGroup)?.generation ?? 9) <= 2;
const filterTest = FILTERS.find((f) => f.key === filterKey).test; const filterTest = FILTERS.find((f) => f.key === filterKey).test;
const frag = document.createDocumentFragment(); const frag = document.createDocumentFragment();
let shown = 0; let shown = 0;
@ -131,6 +135,7 @@ export async function DexGrid() {
const card = Card(species, number, { const card = Card(species, number, {
spriteStyle: st.spriteStyle, spriteStyle: st.spriteStyle,
versionGroup: st.versionGroup, versionGroup: st.versionGroup,
boxed,
}); });
if (shown < 30) card.style.setProperty('--i', String(shown)); if (shown < 30) card.style.setProperty('--i', String(shown));
frag.append(card); frag.append(card);

View File

@ -15,6 +15,16 @@ import { typeHex } from '../lib/type-color.js';
const idFromUrl = (u) => Number(u.replace(/\/$/, '').split('/').pop()); const idFromUrl = (u) => Number(u.replace(/\/$/, '').split('/').pop());
/** A Pokémon's typing as it was in the selected game's generation. */
function typesForGeneration(pokemon, gen) {
const past = (pokemon.past_types || [])
.map((p) => ({ types: p.types, _gen: idFromUrl(p.generation.url) }))
.sort((a, b) => a._gen - b._gen);
const era = past.find((p) => p._gen >= gen);
const list = era ? era.types : pokemon.types;
return list.slice().sort((a, b) => a.slot - b.slot).map((t) => t.type.name);
}
const STAT_NAMES = [ const STAT_NAMES = [
'hp', 'hp',
'attack', 'attack',
@ -68,10 +78,7 @@ export async function PokemonDetail(nationalId) {
return view; return view;
} }
const types = pokemon.types const types = typesForGeneration(pokemon, vgGen);
.slice()
.sort((a, b) => a.slot - b.slot)
.map((t) => t.type.name);
const mainType = types[0]; const mainType = types[0];
view.style.setProperty('--type-main', typeHex(mainType)); view.style.setProperty('--type-main', typeHex(mainType));
view.style.setProperty('--type-2', typeHex(types[1] || mainType)); view.style.setProperty('--type-2', typeHex(types[1] || mainType));
@ -81,8 +88,11 @@ export async function PokemonDetail(nationalId) {
// Shiny Pokémon were introduced in Gen 2 — no toggle for Gen 1 games. // Shiny Pokémon were introduced in Gen 2 — no toggle for Gen 1 games.
const shinyAvailable = vgGen >= 2; const shinyAvailable = vgGen >= 2;
const artStyle = st.spriteStyle === 'default' ? 'official' : st.spriteStyle; const artStyle = st.spriteStyle === 'default' ? 'official' : st.spriteStyle;
const boxedArt = artStyle === 'game' && vgGen <= 2;
let shiny = shinyAvailable && st.showShiny; let shiny = shinyAvailable && st.showShiny;
const artHolder = el('div', { class: 'phero__art' }); const artHolder = el('div', {
class: `phero__art${boxedArt ? ' phero__art--boxed' : ''}`,
});
const shinyBtn = shinyAvailable const shinyBtn = shinyAvailable
? el('button', { ? el('button', {
class: 'phero__shiny', class: 'phero__shiny',