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
* 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 mainType = (species.types || [])[0] || 'normal';
const num = String(number ?? species.id).padStart(3, '0');
const data = { type: mainType, sprite: spriteStyle };
if (boxed) data.boxed = '1';
const caughtBtn = el('button', {
class: 'card__caught',
@ -34,7 +36,7 @@ export function Card(species, number, { spriteStyle = 'official', versionGroup }
{
class: `card${state.caught ? ' is-caught' : ''}`,
href: `#/pokemon/${species.id}`,
dataset: { type: mainType, sprite: spriteStyle },
dataset: data,
style: `--type-main:${typeHex(mainType)}`,
},
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) {
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) {
const m = multiplier(atk, defTypes, gen);
if (m === 1) continue;

View File

@ -450,6 +450,23 @@
.card[data-sprite="game"] .card__art .sprite {
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 {
position: relative;
z-index: 1;
@ -703,6 +720,18 @@
height: 100%;
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 {
from { transform: scale(0.8); opacity: 0; }
to { transform: scale(1); opacity: 1; }

View File

@ -113,6 +113,10 @@ export async function DexGrid() {
function paintGrid() {
const st = settings.get();
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 frag = document.createDocumentFragment();
let shown = 0;
@ -131,6 +135,7 @@ export async function DexGrid() {
const card = Card(species, number, {
spriteStyle: st.spriteStyle,
versionGroup: st.versionGroup,
boxed,
});
if (shown < 30) card.style.setProperty('--i', String(shown));
frag.append(card);

View File

@ -15,6 +15,16 @@ import { typeHex } from '../lib/type-color.js';
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 = [
'hp',
'attack',
@ -68,10 +78,7 @@ export async function PokemonDetail(nationalId) {
return view;
}
const types = pokemon.types
.slice()
.sort((a, b) => a.slot - b.slot)
.map((t) => t.type.name);
const types = typesForGeneration(pokemon, vgGen);
const mainType = types[0];
view.style.setProperty('--type-main', typeHex(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.
const shinyAvailable = vgGen >= 2;
const artStyle = st.spriteStyle === 'default' ? 'official' : st.spriteStyle;
const boxedArt = artStyle === 'game' && vgGen <= 2;
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
? el('button', {
class: 'phero__shiny',