dex/src/components/EvolutionChain.js
chris 671e72972a Apply Prettier to the whole tree
Pure formatting — no behaviour change. Verified: build passes, all 14
routes render, service worker active, no console errors.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
2026-09-10 11:24:31 -04:00

105 lines
3.4 KiB
JavaScript

import { el } from '../lib/dom.js';
import { Sprite } from './Sprite.js';
const idFromUrl = (u) => Number(u.replace(/\/$/, '').split('/').pop());
/** Human-readable trigger for one evolution step. */
function describe(details) {
if (!details || !details.length) return '';
const d = details[0];
if (d.min_level) return `Lv. ${d.min_level}`;
if (d.item) return `use ${d.item.name.replace(/-/g, ' ')}`;
if (d.trigger?.name === 'trade') {
return d.held_item ? `trade w/ ${d.held_item.name.replace(/-/g, ' ')}` : 'trade';
}
if (d.min_happiness) return 'high friendship';
if (d.known_move_type) return `knows a ${d.known_move_type.name} move`;
if (d.trigger?.name) return d.trigger.name.replace(/-/g, ' ');
return '';
}
function nodeEl(stage, style, versionGroup, depth) {
return el(
'a',
{ class: 'evo__node', href: `#/pokemon/${stage.id}`, style: `--d:${depth}` },
Sprite(stage.id, { style, versionGroup, alt: stage.name, size: 56 }),
el('span', { class: 'evo__name' }, stage.name.replace(/-/g, ' ')),
);
}
/**
* PokéAPI returns the full modern evolution tree. Re-parent it around the
* selected game's generation: stages introduced later (cross-gen evos, later
* babies) are dropped, and their in-era descendants attach to the nearest
* surviving ancestor — so Pikachu → Raichu still shows in Red/Blue even
* though Pichu doesn't, and Bellossom never appears there.
*/
export function EvolutionChain(
chain,
{ style = 'default', versionGroup, genOf = () => 1, maxGen = 9 } = {},
) {
const nodes = new Map();
let pruned = false;
const collect = (link, parentId) => {
const id = idFromUrl(link.species.url);
let currentId = parentId;
if (genOf(id) <= maxGen) {
if (!nodes.has(id)) {
nodes.set(id, {
id,
name: link.species.name,
parent: parentId,
via: parentId != null ? describe(link.evolution_details) : '',
kids: [],
});
}
currentId = id;
} else {
pruned = true;
}
for (const child of link.evolves_to) collect(child, currentId);
};
collect(chain.chain, null);
for (const n of nodes.values()) {
if (n.parent != null && nodes.has(n.parent)) nodes.get(n.parent).kids.push(n.id);
}
if (nodes.size <= 1) {
return el(
'p',
{ class: 'detail__muted' },
pruned ? 'No evolutions available in this game.' : 'Does not evolve.',
);
}
const roots = [...nodes.values()].filter((n) => n.parent == null || !nodes.has(n.parent));
const wrap = el('div', { class: 'evo' });
// Vertical tree: a stage, then for each branch a "↓ trigger" step and the
// stage it leads to. Depth adds indentation so mid-chain splits (Wurmple)
// stay grouped without overflowing on a phone.
const renderNode = (n, depth) => {
wrap.append(nodeEl(n, style, versionGroup, depth));
for (const kidId of n.kids) {
const kid = nodes.get(kidId);
wrap.append(
el(
'div',
{ class: 'evo__step', style: `--d:${depth + 1}` },
el('span', { class: 'evo__arrow', 'aria-hidden': 'true' }, '↓'),
el('span', { class: 'evo__via' }, kid.via || 'evolves'),
),
);
renderNode(kid, depth + 1);
}
};
roots.forEach((r) => renderNode(r, 0));
if (pruned) {
wrap.append(el('p', { class: 'evo__note' }, 'Cross-generation stages hidden for this game.'));
}
return wrap;
}