TM/HM numbers; default to the National Dex

- Learnset 'By TM / HM' rows show the machine number for the selected game
  (TM25 / HM05 / TR10), resolved from /machine when the group is expanded
- Move detail page gains a 'TM / HM' fact (marked '(latest)' when the move
  isn't a machine in the currently-selected game)
- machineNumber() exported from MovesList and reused by MoveDetail
- Fresh installs now open on the National Dex (versionGroup 'all') instead
  of Scarlet/Violet

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
chris 2026-08-27 13:56:53 -04:00
parent 0f00b50198
commit 0d95f9c93a
5 changed files with 66 additions and 13 deletions

View File

@ -1,7 +1,24 @@
import { el } from '../lib/dom.js';
import { getMove, mapLimit } from '../data/api.js';
import { getMove, getMachine, mapLimit } from '../data/api.js';
import { TypeChip } from './TypeChip.js';
/** Resolve the TM/HM/TR number for a move in a given game, e.g. "TM25". */
export async function machineNumber(moveData, versionGroupKey) {
const list = moveData.machines || [];
if (!list.length) return null;
const hit =
list.find((x) => x.version_group.name === versionGroupKey) || list[list.length - 1];
try {
const mc = await getMachine(Number(hit.machine.url.replace(/\/$/, '').split('/').pop()));
const m = mc.item.name.match(/^([a-z]+)0*(\d+)$/i);
return m
? `${m[1].toUpperCase()}${String(Number(m[2])).padStart(2, '0')}`
: mc.item.name.toUpperCase();
} catch {
return null;
}
}
const METHOD_LABEL = {
'level-up': 'By level up',
machine: 'By TM / HM',
@ -106,8 +123,17 @@ export function MovesList(pokemonMoves, { versionGroupKey, gen = 9, genOfVg = ()
for (const m of moves) {
const meta = el('span', { class: 'moverow__meta' }, el('span', { class: 'moverow__pending' }, '…'));
const body = el('div', { class: 'movebody', hidden: true });
const tmCell = method === 'machine' ? el('span', { class: 'moverow__tm' }) : null;
let bodyFilled = false;
const fillTm = () => {
if (tmCell && !tmCell.textContent && m.data) {
machineNumber(m.data, versionGroupKey).then((n) => {
if (n) tmCell.textContent = n;
});
}
};
const row = el(
'button',
{
@ -126,6 +152,7 @@ export function MovesList(pokemonMoves, { versionGroupKey, gen = 9, genOfVg = ()
const v = forGeneration(m.data, gen, genOfVg);
fillMeta(meta, v);
fillBody(body, m.data, v);
fillTm();
} catch {
body.replaceChildren(el('span', { class: 'moverow__pending' }, 'Details unavailable.'));
}
@ -134,7 +161,7 @@ export function MovesList(pokemonMoves, { versionGroupKey, gen = 9, genOfVg = ()
},
method === 'level-up'
? el('span', { class: 'moverow__lv' }, m.level ? `Lv ${m.level}` : '—')
: null,
: tmCell,
el('span', { class: 'moverow__name' }, m.name),
meta,
el('span', { class: 'moverow__chev', 'aria-hidden': 'true' }, '▾'),
@ -146,6 +173,7 @@ export function MovesList(pokemonMoves, { versionGroupKey, gen = 9, genOfVg = ()
try {
m.data = m.data || (await getMove(m.id));
fillMeta(meta, forGeneration(m.data, gen, genOfVg));
fillTm();
} catch {
meta.replaceChildren(el('span', { class: 'moverow__pending' }, '—'));
}

View File

@ -26,6 +26,7 @@ export const getSpecies = (idOrName) => getJSON(`pokemon-species/${idOrName}`);
export const getEvolutionChain = (id) => getJSON(`evolution-chain/${id}`);
export const getMove = (idOrName) => getJSON(`move/${idOrName}`);
export const getItem = (idOrName) => getJSON(`item/${idOrName}`);
export const getMachine = (id) => getJSON(`machine/${id}`);
export const getEncounters = (id) => getJSON(`pokemon/${id}/encounters`);
// Full name indexes for the lookup tabs — one request each, then the

View File

@ -11,7 +11,7 @@ import { createStore } from './createStore.js';
* - locale: reserved for localized names (PokéAPI supports many)
*/
export const settings = createStore('pdx.settings', {
versionGroup: 'scarlet-violet',
versionGroup: 'all', // 'all' = National Dex, newest data, no generation limits
pokedex: null,
theme: 'system', // system | light | dark | black | sepia
accent: 'red', // red | blue | green | amber | violet | rose

View File

@ -1256,6 +1256,13 @@
color: var(--text-dim);
font-variant-numeric: tabular-nums;
}
.moverow__tm {
flex: 0 0 3.4rem;
font-weight: 700;
font-size: 0.74rem;
color: var(--accent);
font-variant-numeric: tabular-nums;
}
.moverow__name {
flex: 1 1 7rem;
min-width: 0;

View File

@ -1,6 +1,8 @@
import { el, clear } from '../lib/dom.js';
import { getMove } from '../data/api.js';
import { prettify } from '../data/pokedex-resolver.js';
import { settings } from '../store/settings.js';
import { machineNumber } from '../components/MovesList.js';
import { TypeChip } from '../components/TypeChip.js';
import { typeHex } from '../lib/type-color.js';
@ -62,6 +64,30 @@ export async function MoveDetail(id) {
.join(', ');
if (statChanges) bits.push(statChanges);
const facts = el(
'dl',
{ class: 'pfacts' },
fact('Power', d.power ?? '—'),
fact('Accuracy', d.accuracy != null ? `${d.accuracy}%` : '—'),
fact('PP', d.pp ?? '—'),
fact('Priority', d.priority ?? 0),
fact('Introduced', prettify(d.generation.name)),
fact('Learned by', `${d.learned_by_pokemon.length} Pokémon`),
);
if (d.machines?.length) {
const vg = settings.get().versionGroup;
const machineFact = fact('TM / HM', '…');
facts.append(machineFact);
const exact = d.machines.some((x) => x.version_group.name === vg);
machineNumber(d, vg).then((label) => {
machineFact.querySelector('dd').textContent = label
? exact
? label
: `${label} (latest)`
: '—';
});
}
clear(view).append(
el('nav', { class: 'lookup__nav' }, el('a', { class: 'link', href: '#/search' }, ' Lookup')),
el(
@ -75,16 +101,7 @@ export async function MoveDetail(id) {
el('span', { class: 'lookup__class' }, DMG[d.damage_class?.name] || '—'),
),
),
el(
'dl',
{ class: 'pfacts' },
fact('Power', d.power ?? '—'),
fact('Accuracy', d.accuracy != null ? `${d.accuracy}%` : '—'),
fact('PP', d.pp ?? '—'),
fact('Priority', d.priority ?? 0),
fact('Introduced', prettify(d.generation.name)),
fact('Learned by', `${d.learned_by_pokemon.length} Pokémon`),
),
facts,
el(
'section',
{ class: 'detail__section' },