MovesList: order the TM/HM group by TM number

The learnset's "By TM / HM" group was alphabetical. TM numbers aren't in
the snapshot — they resolve lazily per move via /machine/{id}, which the
group's enrichers already fetch on expand. So: await those resolutions
(fillTm is now async and stashes the label on the move), and once the
whole group is enriched, re-append the rows in TM/HM/TR-then-number order
— TM01, TM02 … HM01 …, matching the in-game list. Verified against
Charizard in FireRed/LeafGreen.

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-08 10:28:27 -04:00
parent 4b9bc5c0ab
commit d73438dd29
2 changed files with 30 additions and 9 deletions

View File

@ -91,7 +91,8 @@ Working app with a type-themed UI:
Moves / Locations); coloured animated stat bars; defensive type matchups; Moves / Locations); coloured animated stat bars; defensive type matchups;
evolution chain re-parented to the selected game's generation; learnset with evolution chain re-parented to the selected game's generation; learnset with
per-move power/type/accuracy/PP/effect (era-accurate via `past_values`, per-move power/type/accuracy/PP/effect (era-accurate via `past_values`,
incl. pre-Gen-4 physical/special-by-type); wild encounter locations. incl. pre-Gen-4 physical/special-by-type), the TM/HM group ordered by
TM number for the selected game; wild encounter locations.
- **Game-aware** — abilities gated to Gen 3+ (hidden to Gen 5+); type chart - **Game-aware** — abilities gated to Gen 3+ (hidden to Gen 5+); type chart
applies Gen 1 / pre-Gen 6 rules. applies Gen 1 / pre-Gen 6 rules.
- **Games** — overlay picker with stylised version-colour cover tiles, - **Games** — overlay picker with stylised version-colour cover tiles,

View File

@ -27,6 +27,15 @@ const METHOD_LABEL = {
'form-change': 'On form change', 'form-change': 'On form change',
}; };
const METHOD_ORDER = ['level-up', 'egg', 'machine', 'tutor', 'form-change']; const METHOD_ORDER = ['level-up', 'egg', 'machine', 'tutor', 'form-change'];
/** Numeric sort key from a TM/HM/TR label like "TM25": TMs, then HMs, then TRs. */
function tmSortKey(label) {
if (!label) return 1e9; // not resolved yet -> sink to the bottom
const m = String(label).match(/^([A-Z]+)0*(\d+)$/);
if (!m) return 1e9 - 1;
const rank = { TM: 0, HM: 1, TR: 2 }[m[1]] ?? 3;
return rank * 1000 + Number(m[2]);
}
const DAMAGE_CLASS = { physical: 'Phys', special: 'Spec', status: 'Stat' }; const DAMAGE_CLASS = { physical: 'Phys', special: 'Spec', status: 'Stat' };
const idFromUrl = (u) => Number(u.replace(/\/$/, '').split('/').pop()); const idFromUrl = (u) => Number(u.replace(/\/$/, '').split('/').pop());
@ -141,11 +150,12 @@ export function MovesList(pokemonMoves, { versionGroupKey, gen = 9, genOfVg = ()
const tmCell = method === 'machine' ? el('span', { class: 'moverow__tm' }) : null; const tmCell = method === 'machine' ? el('span', { class: 'moverow__tm' }) : null;
let bodyFilled = false; let bodyFilled = false;
const fillTm = () => { const fillTm = async () => {
if (tmCell && !tmCell.textContent && m.data) { if (!tmCell || m._tm != null || !m.data) return;
machineNumber(m.data, versionGroupKey).then((n) => { const n = await machineNumber(m.data, versionGroupKey);
if (n) tmCell.textContent = n; if (n) {
}); m._tm = n;
tmCell.textContent = n;
} }
}; };
@ -167,7 +177,7 @@ export function MovesList(pokemonMoves, { versionGroupKey, gen = 9, genOfVg = ()
const v = forGeneration(m.data, gen, genOfVg); const v = forGeneration(m.data, gen, genOfVg);
fillMeta(meta, v); fillMeta(meta, v);
fillBody(body, m.data, v); fillBody(body, m.data, v);
fillTm(); await fillTm();
} catch { } catch {
body.replaceChildren(el('span', { class: 'moverow__pending' }, 'Details unavailable.')); body.replaceChildren(el('span', { class: 'moverow__pending' }, 'Details unavailable.'));
} }
@ -182,13 +192,15 @@ export function MovesList(pokemonMoves, { versionGroupKey, gen = 9, genOfVg = ()
el('span', { class: 'moverow__chev', 'aria-hidden': 'true' }, '▾'), el('span', { class: 'moverow__chev', 'aria-hidden': 'true' }, '▾'),
); );
list.append(el('div', { class: 'movecard' }, row, body)); const card = el('div', { class: 'movecard' }, row, body);
m._card = card;
list.append(card);
enrichers.push(async () => { enrichers.push(async () => {
try { try {
m.data = m.data || (await getMove(m.id)); m.data = m.data || (await getMove(m.id));
fillMeta(meta, forGeneration(m.data, gen, genOfVg)); fillMeta(meta, forGeneration(m.data, gen, genOfVg));
fillTm(); await fillTm();
} catch { } catch {
meta.replaceChildren(el('span', { class: 'moverow__pending' }, '—')); meta.replaceChildren(el('span', { class: 'moverow__pending' }, '—'));
} }
@ -212,6 +224,14 @@ export function MovesList(pokemonMoves, { versionGroupKey, gen = 9, genOfVg = ()
if (enriched) return; if (enriched) return;
enriched = true; enriched = true;
await mapLimit(enrichers, 6, (fn) => fn()); await mapLimit(enrichers, 6, (fn) => fn());
// TM/HM moves land in whatever order the API returned them; reorder
// to TM01, TM02 … once every number has resolved.
if (method === 'machine') {
moves
.slice()
.sort((a, b) => tmSortKey(a._tm) - tmSortKey(b._tm) || a.name.localeCompare(b.name))
.forEach((mm) => list.append(mm._card));
}
}; };
group.addEventListener('toggle', () => group.open && runEnrich()); group.addEventListener('toggle', () => group.open && runEnrich());
if (method === 'level-up') runEnrich(); if (method === 'level-up') runEnrich();