import { el, clear, onTeardown } from '../lib/dom.js'; import { loadSnapshot } from '../data/snapshot.js'; import { settings } from '../store/settings.js'; import { ui } from '../store/ui.js'; import { team, addToTeam, removeFromTeam, clearTeam, MAX_TEAM } from '../store/team.js'; import { TYPES, defenseVector, multiplier } from '../data/type-chart.js'; import { Sprite } from '../components/Sprite.js'; import { TypeChip } from '../components/TypeChip.js'; import { openPokemonPicker } from '../components/PokemonPicker.js'; const prettify = (s) => s.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()); const STAT_ROWS = [ ['hp', 'HP'], ['atk', 'Attack'], ['def', 'Defense'], ['spa', 'Sp. Atk'], ['spd', 'Sp. Def'], ['spe', 'Speed'], ]; const multClass = (m) => m === 0 ? 'm0' : m === 0.25 ? 'm025' : m === 0.5 ? 'm05' : m === 2 ? 'm2' : m === 4 ? 'm4' : 'm1'; const multText = (m) => (m === 0.25 ? '¼' : m === 0.5 ? '½' : m === 1 ? '' : `${m}×`); export async function TeamView() { const view = el('section', { class: 'view teamview' }); const snap = await loadSnapshot(); let mode = ui.get().teamMode === 'compare' ? 'compare' : 'coverage'; const seg = el( 'div', { class: 'seg' }, ...[ ['coverage', 'Coverage'], ['compare', 'Compare'], ].map(([id, label]) => el( 'button', { class: `seg__btn${id === mode ? ' is-active' : ''}`, type: 'button', onclick: () => { mode = id; ui.set({ teamMode: id }); [...seg.children].forEach((b, i) => b.classList.toggle('is-active', ['coverage', 'compare'][i] === id), ); render(); }, }, label, ), ), ); const lineup = el('div', { class: 'lineup' }); const body = el('div', { class: 'teamview__body' }); view.append( el( 'header', { class: 'view__header' }, el('h1', {}, 'Team'), el( 'p', {}, `Up to ${MAX_TEAM} Pokémon — check weaknesses, coverage and stats. `, el('a', { class: 'link', href: '#/natures' }, 'Natures reference'), ), ), seg, lineup, body, ); function members() { return team.get().members.map((id) => snap.speciesById.get(id)).filter(Boolean); } function renderLineup() { const mem = members(); clear(lineup); const style = settings.get().spriteStyle; for (const sp of mem) { lineup.append( el( 'div', { class: 'lineup__slot' }, el( 'button', { class: 'lineup__x', type: 'button', 'aria-label': `Remove ${sp.name}`, onclick: () => { removeFromTeam(sp.id); }, }, '✕', ), el( 'a', { href: `#/pokemon/${sp.id}`, class: 'lineup__link' }, Sprite(sp.id, { style, alt: sp.name, size: 64 }), el('span', { class: 'lineup__name' }, sp.name.replace(/-/g, ' ')), ), ), ); } if (mem.length < MAX_TEAM) { lineup.append( el( 'button', { class: 'lineup__add', type: 'button', onclick: () => openPokemonPicker(addToTeam), }, '+', el('span', {}, 'Add'), ), ); } if (mem.length) { lineup.append( el( 'button', { class: 'lineup__clear', type: 'button', onclick: () => clearTeam() }, 'Clear', ), ); } } function render() { renderLineup(); const mem = members(); clear(body); if (!mem.length) { body.append(el('p', { class: 'detail__muted' }, 'Add some Pokémon to get started.')); return; } body.append(mode === 'coverage' ? coverage(mem) : compare(mem)); } function coverage(mem) { const gen = 9; const vectors = mem.map((sp) => defenseVector(sp.types, gen)); // Defensive weakness table const head = el( 'div', { class: 'cov__row cov__row--head' }, el('span', {}, ''), ...mem.map((sp) => el('span', { class: 'cov__mem' }, Sprite(sp.id, { size: 34, alt: sp.name }))), el('span', { class: 'cov__sum' }, 'Weak'), ); const rows = TYPES.map((t) => { let weak = 0; const cells = vectors.map((v) => { const m = v[t] ?? 1; if (m >= 2) weak += 1; return el('span', { class: `cov__cell ${multClass(m)}` }, multText(m)); }); return el( 'div', { class: 'cov__row' }, el('span', { class: 'cov__type' }, TypeChip(t)), ...cells, el('span', { class: `cov__sum${weak >= 2 ? ' is-bad' : ''}` }, weak || ''), ); }); // Offensive coverage gaps: defending types no member hits super-effectively const gaps = TYPES.filter((def) => { if (gen < 6 && def === 'fairy') return false; return !mem.some((sp) => sp.types.some((atk) => multiplier(atk, [def], gen) >= 2)); }); // Quick team stats const avgBst = Math.round(mem.reduce((s, m) => s + (m.bst || 0), 0) / mem.length); const fastest = mem.slice().sort((a, b) => (b.stats?.spe || 0) - (a.stats?.spe || 0))[0]; const bulkiest = mem .slice() .sort( (a, b) => (b.stats?.hp || 0) + (b.stats?.def || 0) + (b.stats?.spd || 0) - ((a.stats?.hp || 0) + (a.stats?.def || 0) + (a.stats?.spd || 0)), )[0]; return el( 'div', {}, el('h2', { class: 'ppanel__sub' }, 'Type coverage — how each type hits your team'), el('div', { class: 'cov' }, head, ...rows), el('h2', { class: 'ppanel__sub' }, 'Offensive gaps (no super-effective STAB)'), gaps.length ? el('div', { class: 'matchups__types' }, ...gaps.map(TypeChip)) : el('p', { class: 'detail__muted' }, 'Your team hits every type super-effectively. Nice.'), el('h2', { class: 'ppanel__sub' }, 'At a glance'), el( 'dl', { class: 'pfacts' }, fact('Average BST', String(avgBst)), fact('Fastest', `${prettify(fastest.name)} (${fastest.stats?.spe ?? '?'} Spe)`), fact( 'Bulkiest', `${prettify(bulkiest.name)} (${ (bulkiest.stats?.hp || 0) + (bulkiest.stats?.def || 0) + (bulkiest.stats?.spd || 0) } HP+Def+SpD)`, ), ), ); } function compare(mem) { const maxOf = (key) => Math.max(...mem.map((m) => m.stats?.[key] ?? 0)); const row = (label, cells, cls = '') => el('div', { class: `cmp__row ${cls}` }, el('span', { class: 'cmp__label' }, label), ...cells); return el( 'div', { class: 'cmp' }, row( '', mem.map((sp) => el( 'a', { class: 'cmp__mem', href: `#/pokemon/${sp.id}` }, Sprite(sp.id, { size: 44, alt: sp.name }), el('span', {}, sp.name.replace(/-/g, ' ')), ), ), 'cmp__row--head', ), row( 'Type', mem.map((sp) => el('span', { class: 'cmp__cell' }, ...sp.types.map(TypeChip))), ), ...STAT_ROWS.map(([key, label]) => { const mx = maxOf(key); return row( label, mem.map((sp) => { const v = sp.stats?.[key] ?? 0; const bar = el('span', { class: 'cmp__bar' }); bar.style.width = `${(v / 200) * 100}%`; return el( 'span', { class: `cmp__cell cmp__stat${v === mx ? ' is-max' : ''}` }, el('span', { class: 'cmp__val' }, String(v)), el('span', { class: 'cmp__track' }, bar), ); }), ); }), row( 'BST', mem.map((sp) => { const mx = Math.max(...mem.map((m) => m.bst || 0)); return el( 'span', { class: `cmp__cell cmp__stat${(sp.bst || 0) === mx ? ' is-max' : ''}` }, el('span', { class: 'cmp__val' }, String(sp.bst || 0)), ); }), 'cmp__row--bst', ), ); } render(); const off = team.subscribe(render); const offStyle = settings.subscribe(renderLineup); onTeardown(view, () => { off(); offStyle(); }); return view; } function fact(k, v) { return el('div', { class: 'fact' }, el('dt', {}, k), el('dd', {}, v)); }