Compare commits
No commits in common. "86e9bc60bc9ccd27da777bced03e3e387ed6e6e6" and "4b9bc5c0abe3c4788a56ff11e3915ba03b6cc826" have entirely different histories.
86e9bc60bc
...
4b9bc5c0ab
16
README.md
16
README.md
@ -91,20 +91,16 @@ 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), the TM/HM group ordered by
|
incl. pre-Gen-4 physical/special-by-type); wild encounter locations.
|
||||||
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,
|
||||||
sub-dex switch, and an "All games" (National, no gen limits) option.
|
sub-dex switch, and an "All games" (National, no gen limits) option.
|
||||||
- **Search** — tabbed lookup: Pokémon, Moves, Items and Abilities, all
|
- **Search** — tabbed lookup: Pokémon, Moves and Items, all browsable
|
||||||
browsable offline from the snapshot. Moves filter by type / damage class
|
offline from the snapshot. Moves filter by type / damage class / "TMs in
|
||||||
/ "TMs only" / "HMs only" for the selected game and sort by power /
|
this game" and sort by power / accuracy / recency; Items filter by
|
||||||
accuracy / recency — or, with a machine filter on, by TM/HM number
|
category. Each has its own detail page. Query, scroll, tab and filters
|
||||||
(numbers are baked into the snapshot). Items filter by category. Each
|
persist.
|
||||||
has its own detail page — the move page lists every Pokémon that can
|
|
||||||
learn it (filterable, narrowed to species that existed by the selected
|
|
||||||
game's generation). Query, scroll, tab and filters persist.
|
|
||||||
- **Team** — a lineup of up to 6. Coverage leads with a "weak spots"
|
- **Team** — a lineup of up to 6. Coverage leads with a "weak spots"
|
||||||
summary (types that hit 2+ members, or that someone's weak to and nobody
|
summary (types that hit 2+ members, or that someone's weak to and nobody
|
||||||
resists), then a full matchup grid with a diverging resist◀│▶weak bar per
|
resists), then a full matchup grid with a diverging resist◀│▶weak bar per
|
||||||
|
|||||||
@ -335,7 +335,7 @@ async function main() {
|
|||||||
pp: m.pp,
|
pp: m.pp,
|
||||||
priority: m.priority,
|
priority: m.priority,
|
||||||
generation: idFromUrl(m.generation.url),
|
generation: idFromUrl(m.generation.url),
|
||||||
// version-group -> machine id; `tm` label filled in by the pass below
|
// version-group -> machine id (resolve to TM/HM number on demand)
|
||||||
machines: (m.machines || []).map((x) => ({
|
machines: (m.machines || []).map((x) => ({
|
||||||
vg: x.version_group.name,
|
vg: x.version_group.name,
|
||||||
id: idFromUrl(x.machine.url),
|
id: idFromUrl(x.machine.url),
|
||||||
@ -344,29 +344,6 @@ async function main() {
|
|||||||
});
|
});
|
||||||
moves.sort((a, b) => a.id - b.id);
|
moves.sort((a, b) => a.id - b.id);
|
||||||
|
|
||||||
// ---- TM / HM / TR numbers ---------------------------------------
|
|
||||||
// Resolve every machine id to its item name ("tm25" -> "TM25") so the
|
|
||||||
// search UI can show and sort by TM number without ~2000 lazy lookups.
|
|
||||||
const machineIds = [...new Set(moves.flatMap((m) => m.machines.map((x) => x.id)))];
|
|
||||||
console.log(`Fetching ${machineIds.length} TM/HM numbers …`);
|
|
||||||
const tmById = new Map();
|
|
||||||
await mapLimit(machineIds, CONCURRENCY, async (mid) => {
|
|
||||||
try {
|
|
||||||
const mc = await api(`machine/${mid}`);
|
|
||||||
const raw = mc.item?.name || '';
|
|
||||||
const mm = raw.match(/^([a-z]+)0*(\d+)$/i);
|
|
||||||
tmById.set(
|
|
||||||
mid,
|
|
||||||
mm ? `${mm[1].toUpperCase()}${String(Number(mm[2])).padStart(2, '0')}` : raw.toUpperCase(),
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
/* leave unresolved */
|
|
||||||
}
|
|
||||||
});
|
|
||||||
for (const m of moves) {
|
|
||||||
for (const x of m.machines) x.tm = tmById.get(x.id) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Abilities index ------------------------------------------
|
// ---- Abilities index ------------------------------------------
|
||||||
const abilityIndex = await api('ability?limit=100000');
|
const abilityIndex = await api('ability?limit=100000');
|
||||||
console.log(`Fetching ${abilityIndex.results.length} abilities …`);
|
console.log(`Fetching ${abilityIndex.results.length} abilities …`);
|
||||||
|
|||||||
@ -27,15 +27,6 @@ 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());
|
||||||
@ -150,12 +141,11 @@ 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 = async () => {
|
const fillTm = () => {
|
||||||
if (!tmCell || m._tm != null || !m.data) return;
|
if (tmCell && !tmCell.textContent && m.data) {
|
||||||
const n = await machineNumber(m.data, versionGroupKey);
|
machineNumber(m.data, versionGroupKey).then((n) => {
|
||||||
if (n) {
|
if (n) tmCell.textContent = n;
|
||||||
m._tm = n;
|
});
|
||||||
tmCell.textContent = n;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -177,7 +167,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);
|
||||||
await fillTm();
|
fillTm();
|
||||||
} catch {
|
} catch {
|
||||||
body.replaceChildren(el('span', { class: 'moverow__pending' }, 'Details unavailable.'));
|
body.replaceChildren(el('span', { class: 'moverow__pending' }, 'Details unavailable.'));
|
||||||
}
|
}
|
||||||
@ -192,15 +182,13 @@ export function MovesList(pokemonMoves, { versionGroupKey, gen = 9, genOfVg = ()
|
|||||||
el('span', { class: 'moverow__chev', 'aria-hidden': 'true' }, '▾'),
|
el('span', { class: 'moverow__chev', 'aria-hidden': 'true' }, '▾'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const card = el('div', { class: 'movecard' }, row, body);
|
list.append(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));
|
||||||
await fillTm();
|
fillTm();
|
||||||
} catch {
|
} catch {
|
||||||
meta.replaceChildren(el('span', { class: 'moverow__pending' }, '—'));
|
meta.replaceChildren(el('span', { class: 'moverow__pending' }, '—'));
|
||||||
}
|
}
|
||||||
@ -224,14 +212,6 @@ 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();
|
||||||
|
|||||||
@ -26,7 +26,6 @@ export const ui = createStore('pdx.ui', {
|
|||||||
mvClass: '',
|
mvClass: '',
|
||||||
mvSort: 'name',
|
mvSort: 'name',
|
||||||
mvTm: false,
|
mvTm: false,
|
||||||
mvHm: false,
|
|
||||||
itCat: '',
|
itCat: '',
|
||||||
itSort: 'name',
|
itSort: 'name',
|
||||||
abSort: 'name',
|
abSort: 'name',
|
||||||
|
|||||||
@ -2700,9 +2700,6 @@
|
|||||||
grid-template-columns: repeat(auto-fill, minmax(88px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(88px, 1fr));
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.learners__filter {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.ab-mon {
|
.ab-mon {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@ -1,15 +1,12 @@
|
|||||||
import { el, clear } from '../lib/dom.js';
|
import { el, clear } from '../lib/dom.js';
|
||||||
import { getMove } from '../data/api.js';
|
import { getMove } from '../data/api.js';
|
||||||
import { loadSnapshot } from '../data/snapshot.js';
|
|
||||||
import { prettify } from '../data/pokedex-resolver.js';
|
import { prettify } from '../data/pokedex-resolver.js';
|
||||||
import { settings } from '../store/settings.js';
|
import { settings } from '../store/settings.js';
|
||||||
import { machineNumber } from '../components/MovesList.js';
|
import { machineNumber } from '../components/MovesList.js';
|
||||||
import { Sprite } from '../components/Sprite.js';
|
|
||||||
import { TypeChip } from '../components/TypeChip.js';
|
import { TypeChip } from '../components/TypeChip.js';
|
||||||
import { typeHex } from '../lib/type-color.js';
|
import { typeHex } from '../lib/type-color.js';
|
||||||
|
|
||||||
const DMG = { physical: 'Physical', special: 'Special', status: 'Status' };
|
const DMG = { physical: 'Physical', special: 'Special', status: 'Status' };
|
||||||
const idFromUrl = (u) => Number(u.replace(/\/$/, '').split('/').pop());
|
|
||||||
|
|
||||||
const english = (entries, key) => {
|
const english = (entries, key) => {
|
||||||
const hit = (entries || []).find((e) => e.language.name === 'en');
|
const hit = (entries || []).find((e) => e.language.name === 'en');
|
||||||
@ -24,8 +21,6 @@ export async function MoveDetail(id) {
|
|||||||
const view = el('section', { class: 'view lookup' });
|
const view = el('section', { class: 'view lookup' });
|
||||||
view.append(el('div', { class: 'view--loading' }, 'Loading move…'));
|
view.append(el('div', { class: 'view--loading' }, 'Loading move…'));
|
||||||
|
|
||||||
const snap = await loadSnapshot();
|
|
||||||
|
|
||||||
let d;
|
let d;
|
||||||
try {
|
try {
|
||||||
d = await getMove(id);
|
d = await getMove(id);
|
||||||
@ -69,17 +64,6 @@ export async function MoveDetail(id) {
|
|||||||
.join(', ');
|
.join(', ');
|
||||||
if (statChanges) bits.push(statChanges);
|
if (statChanges) bits.push(statChanges);
|
||||||
|
|
||||||
// Pokémon that can learn this move. PokéAPI only gives a flat, all-games
|
|
||||||
// list — narrow it to species that existed by the selected game's
|
|
||||||
// generation so a "Crystal" view isn't full of Gen 9 Pokémon.
|
|
||||||
const vg = settings.get().versionGroup;
|
|
||||||
const gen = snap.versionGroupByKey.get(vg)?.generation ?? 9;
|
|
||||||
let learners = (d.learned_by_pokemon || [])
|
|
||||||
.map((p) => snap.speciesById.get(idFromUrl(p.url)))
|
|
||||||
.filter(Boolean);
|
|
||||||
if (vg !== 'all') learners = learners.filter((sp) => sp.generation <= gen);
|
|
||||||
learners.sort((a, b) => a.id - b.id);
|
|
||||||
|
|
||||||
const facts = el(
|
const facts = el(
|
||||||
'dl',
|
'dl',
|
||||||
{ class: 'pfacts' },
|
{ class: 'pfacts' },
|
||||||
@ -88,7 +72,7 @@ export async function MoveDetail(id) {
|
|||||||
fact('PP', d.pp ?? '—'),
|
fact('PP', d.pp ?? '—'),
|
||||||
fact('Priority', d.priority ?? 0),
|
fact('Priority', d.priority ?? 0),
|
||||||
fact('Introduced', prettify(d.generation.name)),
|
fact('Introduced', prettify(d.generation.name)),
|
||||||
fact('Learned by', `${learners.length} Pokémon`),
|
fact('Learned by', `${d.learned_by_pokemon.length} Pokémon`),
|
||||||
);
|
);
|
||||||
if (d.machines?.length) {
|
if (d.machines?.length) {
|
||||||
const vg = settings.get().versionGroup;
|
const vg = settings.get().versionGroup;
|
||||||
@ -104,56 +88,6 @@ export async function MoveDetail(id) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Learners grid (filterable) ----------------------------------
|
|
||||||
const style = settings.get().spriteStyle;
|
|
||||||
const LEARNER_CAP = 400;
|
|
||||||
const learnersGrid = el('div', { class: 'ab-mons' });
|
|
||||||
function paintLearners(q) {
|
|
||||||
const qq = (q || '').trim().toLowerCase().replace(/\s+/g, '-');
|
|
||||||
const shown = qq ? learners.filter((sp) => sp.name.includes(qq)) : learners;
|
|
||||||
clear(learnersGrid);
|
|
||||||
if (!shown.length) {
|
|
||||||
learnersGrid.append(el('p', { class: 'detail__muted' }, 'No matches.'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const sp of shown.slice(0, LEARNER_CAP)) {
|
|
||||||
learnersGrid.append(
|
|
||||||
el(
|
|
||||||
'a',
|
|
||||||
{ class: 'ab-mon', href: `#/pokemon/${sp.id}` },
|
|
||||||
Sprite(sp.id, { style, alt: sp.name, size: 48 }),
|
|
||||||
el('span', {}, sp.name.replace(/-/g, ' ')),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (shown.length > LEARNER_CAP) {
|
|
||||||
learnersGrid.append(
|
|
||||||
el('p', { class: 'detail__muted' }, `+${shown.length - LEARNER_CAP} more — filter to narrow.`),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
paintLearners('');
|
|
||||||
const learnerFilter = el('input', {
|
|
||||||
type: 'search',
|
|
||||||
class: 'search__input learners__filter',
|
|
||||||
placeholder: 'Filter Pokémon…',
|
|
||||||
oninput: (e) => paintLearners(e.target.value),
|
|
||||||
});
|
|
||||||
|
|
||||||
const learnersSection = learners.length
|
|
||||||
? el(
|
|
||||||
'section',
|
|
||||||
{ class: 'detail__section' },
|
|
||||||
el(
|
|
||||||
'h2',
|
|
||||||
{},
|
|
||||||
`Pokémon that can learn this${vg === 'all' ? '' : ` in ${prettify(vg)}`} (${learners.length})`,
|
|
||||||
),
|
|
||||||
learners.length > 24 ? learnerFilter : null,
|
|
||||||
learnersGrid,
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
clear(view).append(
|
clear(view).append(
|
||||||
el('nav', { class: 'lookup__nav' }, el('a', { class: 'link', href: '#/search' }, '‹ Lookup')),
|
el('nav', { class: 'lookup__nav' }, el('a', { class: 'link', href: '#/search' }, '‹ Lookup')),
|
||||||
el(
|
el(
|
||||||
@ -175,7 +109,6 @@ export async function MoveDetail(id) {
|
|||||||
el('p', {}, effect),
|
el('p', {}, effect),
|
||||||
bits.length ? el('p', { class: 'movebody__bits' }, bits.join(' · ')) : null,
|
bits.length ? el('p', { class: 'movebody__bits' }, bits.join(' · ')) : null,
|
||||||
),
|
),
|
||||||
learnersSection,
|
|
||||||
);
|
);
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { settings } from '../store/settings.js';
|
|||||||
import { ui } from '../store/ui.js';
|
import { ui } from '../store/ui.js';
|
||||||
import { entry } from '../store/selection.js';
|
import { entry } from '../store/selection.js';
|
||||||
import { prettify } from '../data/pokedex-resolver.js';
|
import { prettify } from '../data/pokedex-resolver.js';
|
||||||
|
import { getMachine, mapLimit } from '../data/api.js';
|
||||||
import { Sprite } from '../components/Sprite.js';
|
import { Sprite } from '../components/Sprite.js';
|
||||||
import { TypeChip } from '../components/TypeChip.js';
|
import { TypeChip } from '../components/TypeChip.js';
|
||||||
|
|
||||||
@ -24,12 +25,9 @@ const TABS = [
|
|||||||
{ id: 'abilities', label: 'Abilities' },
|
{ id: 'abilities', label: 'Abilities' },
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Numeric sort key from a TM/HM/TR label like "TM25": TMs, then HMs, then TRs. */
|
const tmLabel = (raw) => {
|
||||||
const tmSortKey = (label) => {
|
const m = raw.match(/^([a-z]+)0*(\d+)$/i);
|
||||||
if (!label) return 1e9;
|
return m ? `${m[1].toUpperCase()}${String(Number(m[2])).padStart(2, '0')}` : raw.toUpperCase();
|
||||||
const m = String(label).match(/^([A-Z]+)0*(\d+)$/);
|
|
||||||
if (!m) return 1e9 - 1;
|
|
||||||
return (({ TM: 0, HM: 1, TR: 2 })[m[1]] ?? 3) * 1000 + Number(m[2]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function SearchView() {
|
export async function SearchView() {
|
||||||
@ -38,6 +36,8 @@ export async function SearchView() {
|
|||||||
|
|
||||||
let tab = TABS.some((t) => t.id === ui.get().searchTab) ? ui.get().searchTab : 'pokemon';
|
let tab = TABS.some((t) => t.id === ui.get().searchTab) ? ui.get().searchTab : 'pokemon';
|
||||||
let query = ui.get().searchQuery || '';
|
let query = ui.get().searchQuery || '';
|
||||||
|
let enrichTimer = null;
|
||||||
|
let runToken = 0;
|
||||||
|
|
||||||
const input = el('input', {
|
const input = el('input', {
|
||||||
class: 'search__input',
|
class: 'search__input',
|
||||||
@ -85,53 +85,10 @@ export async function SearchView() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// "TMs only" / "HMs only" checkboxes. Toggling either rebuilds the filter
|
|
||||||
// row so the sort dropdown gains/loses "TM / HM number", and drops that
|
|
||||||
// sort if it's active once neither is on.
|
|
||||||
function machineToggle(key, label) {
|
|
||||||
return el(
|
|
||||||
'label',
|
|
||||||
{ class: 'lookup__check' },
|
|
||||||
el('input', {
|
|
||||||
type: 'checkbox',
|
|
||||||
checked: !!ui.get()[key],
|
|
||||||
onchange: (e) => {
|
|
||||||
ui.set({ [key]: e.target.checked });
|
|
||||||
if (!ui.get().mvTm && !ui.get().mvHm && ui.get().mvSort === 'tm') {
|
|
||||||
ui.set({ mvSort: 'name' });
|
|
||||||
}
|
|
||||||
buildFilters();
|
|
||||||
run();
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
label,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildFilters() {
|
function buildFilters() {
|
||||||
clear(filters);
|
clear(filters);
|
||||||
if (tab === 'moves') {
|
if (tab === 'moves') {
|
||||||
const vg = settings.get().versionGroup;
|
|
||||||
const gameHasKind = (kind) =>
|
|
||||||
snap.moves.some((m) =>
|
|
||||||
m.machines.some(
|
|
||||||
(x) => (vg === 'all' || x.vg === vg) && (x.tm || '').startsWith(kind),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const hasTm = gameHasKind('TM');
|
|
||||||
const hasHm = gameHasKind('HM'); // false for Gen 8+ games, which dropped HMs
|
|
||||||
|
|
||||||
// A machine filter left on for a game that has no such machines would
|
|
||||||
// silently zero the list with no visible checkbox to switch it off.
|
|
||||||
if ((!hasTm && ui.get().mvTm) || (!hasHm && ui.get().mvHm)) {
|
|
||||||
ui.set({ mvTm: hasTm && ui.get().mvTm, mvHm: hasHm && ui.get().mvHm });
|
|
||||||
if (!ui.get().mvTm && !ui.get().mvHm && ui.get().mvSort === 'tm') {
|
|
||||||
ui.set({ mvSort: 'name' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
filters.append(
|
filters.append(
|
||||||
...[
|
|
||||||
pill('mvType', [['', 'Any type'], ...TYPES.map((t) => [t, prettify(t)])], ui.get().mvType, (v) => {
|
pill('mvType', [['', 'Any type'], ...TYPES.map((t) => [t, prettify(t)])], ui.get().mvType, (v) => {
|
||||||
ui.set({ mvType: v });
|
ui.set({ mvType: v });
|
||||||
run();
|
run();
|
||||||
@ -147,23 +104,26 @@ export async function SearchView() {
|
|||||||
),
|
),
|
||||||
pill(
|
pill(
|
||||||
'mvSort',
|
'mvSort',
|
||||||
[
|
[['name', 'A–Z'], ['power', 'Power'], ['accuracy', 'Accuracy'], ['gen', 'Newest']],
|
||||||
['name', 'A–Z'],
|
|
||||||
['power', 'Power'],
|
|
||||||
['accuracy', 'Accuracy'],
|
|
||||||
['gen', 'Newest'],
|
|
||||||
// "By number" only makes sense with a TM/HM filter on.
|
|
||||||
...(ui.get().mvTm || ui.get().mvHm ? [['tm', 'TM / HM number']] : []),
|
|
||||||
],
|
|
||||||
ui.get().mvSort,
|
ui.get().mvSort,
|
||||||
(v) => {
|
(v) => {
|
||||||
ui.set({ mvSort: v });
|
ui.set({ mvSort: v });
|
||||||
run();
|
run();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
hasTm ? machineToggle('mvTm', 'TMs only') : null,
|
el(
|
||||||
hasHm ? machineToggle('mvHm', 'HMs only') : null,
|
'label',
|
||||||
].filter(Boolean),
|
{ class: 'lookup__check' },
|
||||||
|
el('input', {
|
||||||
|
type: 'checkbox',
|
||||||
|
checked: !!ui.get().mvTm,
|
||||||
|
onchange: (e) => {
|
||||||
|
ui.set({ mvTm: e.target.checked });
|
||||||
|
run();
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'TMs only',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else if (tab === 'items') {
|
} else if (tab === 'items') {
|
||||||
filters.append(
|
filters.append(
|
||||||
@ -245,41 +205,19 @@ export async function SearchView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---- Moves (from snapshot, offline) -----------------------------
|
// ---- Moves (from snapshot, offline) -----------------------------
|
||||||
function renderMoves(q) {
|
function renderMoves(q, token) {
|
||||||
const st = settings.get();
|
const st = settings.get();
|
||||||
const vg = st.versionGroup;
|
const vg = st.versionGroup;
|
||||||
const type = ui.get().mvType;
|
const type = ui.get().mvType;
|
||||||
const cls = ui.get().mvClass;
|
const cls = ui.get().mvClass;
|
||||||
const sort = ui.get().mvSort;
|
const sort = ui.get().mvSort;
|
||||||
const tmOnly = !!ui.get().mvTm;
|
const tmOnly = !!ui.get().mvTm;
|
||||||
const hmOnly = !!ui.get().mvHm;
|
|
||||||
const machineFilter = tmOnly || hmOnly;
|
|
||||||
const wantKinds = [tmOnly && 'TM', hmOnly && 'HM'].filter(Boolean);
|
|
||||||
const kindOf = (x) => (x.tm || '').slice(0, 2); // TM | HM | TR
|
|
||||||
|
|
||||||
// The machine entry to show / sort / filter by. For a specific game
|
|
||||||
// it's that game's entry; for National ("all") — where a move can be a
|
|
||||||
// TM in one game and an HM in another — it's the newest entry of a kind
|
|
||||||
// the filter is asking for. TM/HM numbers are baked into the snapshot.
|
|
||||||
const machineFor = (m) => {
|
|
||||||
if (vg !== 'all') return m.machines.find((x) => x.vg === vg) || null;
|
|
||||||
if (wantKinds.length) {
|
|
||||||
return [...m.machines].reverse().find((x) => wantKinds.includes(kindOf(x))) || null;
|
|
||||||
}
|
|
||||||
return m.machines[m.machines.length - 1] || null;
|
|
||||||
};
|
|
||||||
|
|
||||||
let list = snap.moves.filter((m) => {
|
let list = snap.moves.filter((m) => {
|
||||||
if (q && !loose(m.name).includes(q)) return false;
|
if (q && !loose(m.name).includes(q)) return false;
|
||||||
if (type && m.type !== type) return false;
|
if (type && m.type !== type) return false;
|
||||||
if (cls && m.damageClass !== cls) return false;
|
if (cls && m.damageClass !== cls) return false;
|
||||||
if (machineFilter) {
|
if (tmOnly && !m.machines.some((x) => vg === 'all' || x.vg === vg)) return false;
|
||||||
const e = machineFor(m);
|
|
||||||
if (!e) return false;
|
|
||||||
// machineFor already restricted the kind for National; for a
|
|
||||||
// specific game, check the game's own entry is the right kind.
|
|
||||||
if (vg !== 'all' && !wantKinds.includes(kindOf(e))) return false;
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -287,22 +225,16 @@ export async function SearchView() {
|
|||||||
if (sort === 'power') return (b.power ?? -1) - (a.power ?? -1) || a.name.localeCompare(b.name);
|
if (sort === 'power') return (b.power ?? -1) - (a.power ?? -1) || a.name.localeCompare(b.name);
|
||||||
if (sort === 'accuracy') return (b.accuracy ?? -1) - (a.accuracy ?? -1) || a.name.localeCompare(b.name);
|
if (sort === 'accuracy') return (b.accuracy ?? -1) - (a.accuracy ?? -1) || a.name.localeCompare(b.name);
|
||||||
if (sort === 'gen') return b.generation - a.generation || a.name.localeCompare(b.name);
|
if (sort === 'gen') return b.generation - a.generation || a.name.localeCompare(b.name);
|
||||||
if (sort === 'tm') {
|
|
||||||
return (
|
|
||||||
tmSortKey(machineFor(a)?.tm) - tmSortKey(machineFor(b)?.tm) ||
|
|
||||||
a.name.localeCompare(b.name)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return a.name.localeCompare(b.name);
|
return a.name.localeCompare(b.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
note.textContent = `${list.length} move${list.length === 1 ? '' : 's'}`;
|
note.textContent = `${list.length} move${list.length === 1 ? '' : 's'}`;
|
||||||
const shown = list.slice(0, 400);
|
const shown = list.slice(0, 400);
|
||||||
|
const machineCells = new Map();
|
||||||
|
|
||||||
for (const m of shown) {
|
for (const m of shown) {
|
||||||
const tmCell = machineFilter
|
const tmCell = tmOnly ? el('span', { class: 'moverow__tm' }) : null;
|
||||||
? el('span', { class: 'moverow__tm' }, machineFor(m)?.tm || '')
|
if (tmCell) machineCells.set(m.id, { cell: tmCell, m });
|
||||||
: null;
|
|
||||||
results.append(
|
results.append(
|
||||||
el(
|
el(
|
||||||
'a',
|
'a',
|
||||||
@ -322,6 +254,23 @@ export async function SearchView() {
|
|||||||
if (list.length > shown.length) {
|
if (list.length > shown.length) {
|
||||||
results.append(el('p', { class: 'search__empty' }, `Showing ${shown.length} of ${list.length} — refine to see more.`));
|
results.append(el('p', { class: 'search__empty' }, `Showing ${shown.length} of ${list.length} — refine to see more.`));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tmOnly && machineCells.size) {
|
||||||
|
clearTimeout(enrichTimer);
|
||||||
|
enrichTimer = setTimeout(() => {
|
||||||
|
mapLimit([...machineCells.values()], 6, async ({ cell, m }) => {
|
||||||
|
if (token !== runToken || !cell.isConnected) return;
|
||||||
|
const hit = m.machines.find((x) => x.vg === vg) || m.machines[m.machines.length - 1];
|
||||||
|
if (!hit) return;
|
||||||
|
try {
|
||||||
|
const mc = await getMachine(hit.id);
|
||||||
|
cell.textContent = tmLabel(mc.item.name);
|
||||||
|
} catch {
|
||||||
|
/* leave blank */
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Items (from snapshot, offline) ---------------------------
|
// ---- Items (from snapshot, offline) ---------------------------
|
||||||
@ -382,6 +331,7 @@ export async function SearchView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function run() {
|
function run() {
|
||||||
|
const token = ++runToken;
|
||||||
const q = loose(query);
|
const q = loose(query);
|
||||||
clear(results);
|
clear(results);
|
||||||
note.textContent = '';
|
note.textContent = '';
|
||||||
@ -393,26 +343,15 @@ export async function SearchView() {
|
|||||||
if (q) renderPokemon(q);
|
if (q) renderPokemon(q);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (tab === 'moves') renderMoves(q);
|
if (tab === 'moves') renderMoves(q, token);
|
||||||
else if (tab === 'items') renderItems(q);
|
else if (tab === 'items') renderItems(q);
|
||||||
else renderAbilities(q);
|
else renderAbilities(q);
|
||||||
}
|
}
|
||||||
|
|
||||||
run();
|
run();
|
||||||
requestAnimationFrame(() => window.scrollTo(0, ui.get().searchScroll || 0));
|
requestAnimationFrame(() => window.scrollTo(0, ui.get().searchScroll || 0));
|
||||||
|
|
||||||
// Switching games (from the Games sheet) changes which TM/HM filters
|
|
||||||
// apply and the numbers shown — refresh the filter row and results.
|
|
||||||
let lastVg = settings.get().versionGroup;
|
|
||||||
const offSettings = settings.subscribe((s) => {
|
|
||||||
if (s.versionGroup === lastVg) return;
|
|
||||||
lastVg = s.versionGroup;
|
|
||||||
syncUI();
|
|
||||||
run();
|
|
||||||
});
|
|
||||||
|
|
||||||
onTeardown(view, () => {
|
onTeardown(view, () => {
|
||||||
offSettings();
|
clearTimeout(enrichTimer);
|
||||||
ui.set({ searchScroll: window.scrollY });
|
ui.set({ searchScroll: window.scrollY });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user