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; }