Forms in the team
Team slots are now { id, form } instead of bare ids (old saves migrate
on read via slot()). The detail page's team button adds whatever
mechanical form is selected — "+ Mega X" / "✓ Mega X in team" — and
TeamView merges that form's baked snapshot types/stats/BST into the
member so Coverage, Compare and the Calc all analyse the form (Mega
Charizard X shows as Fire/Dragon, BST 634). Lineup shows the form name
and sprite; remove targets the exact slot. The Add picker still adds
base forms — Megas etc. come from their own page.
CompareTable takes an optional spriteId; addToTeam/removeFromTeam/inTeam
gain a form argument.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
This commit is contained in:
parent
b15ee3dd7f
commit
7f12f08718
@ -18,7 +18,8 @@ framework.
|
||||
- **Detail data** (stats, abilities, flavor text, evolution) → fetched lazily from `pokeapi.co` per Pokémon, then cached by the service worker (stale-while-revalidate, 30-day TTL).
|
||||
- **Sprites** → cache-first with a capped LRU.
|
||||
|
||||
Export / import of `settings` + `selection` as JSON lives in **Settings**.
|
||||
Export / import of all local state (settings, tracking, team, form
|
||||
tracking, shiny hunts) as JSON lives in **Settings**.
|
||||
|
||||
## Scripts
|
||||
|
||||
|
||||
@ -31,7 +31,7 @@ export function CompareTable(members, { gen = 9, style } = {}) {
|
||||
el(
|
||||
'a',
|
||||
{ class: 'cmp__mem', href: `#/pokemon/${sp.id}` },
|
||||
Sprite(sp.id, { style, size: 44, alt: sp.name }),
|
||||
Sprite(sp.spriteId ?? sp.id, { style, size: 44, alt: sp.name }),
|
||||
el('span', {}, sp.name.replace(/-/g, ' ')),
|
||||
),
|
||||
),
|
||||
|
||||
@ -1,28 +1,45 @@
|
||||
import { createStore } from './createStore.js';
|
||||
|
||||
/**
|
||||
* The working "lineup" — up to 6 Pokémon (national dex ids) analysed by the
|
||||
* Team view (coverage) and Compare view. Persisted.
|
||||
* The working "lineup" — up to 6 slots analysed by the Team view (coverage
|
||||
* / compare / calc). A slot is `{ id, form }` where `id` is the national
|
||||
* dex id and `form` is a snapshot form slug (Mega, regional, alt forme) or
|
||||
* null for the base Pokémon. Persisted; old saves stored bare ids and are
|
||||
* migrated on read.
|
||||
*/
|
||||
export const team = createStore('pdx.team', { members: [] });
|
||||
|
||||
export const MAX_TEAM = 6;
|
||||
|
||||
export function addToTeam(id) {
|
||||
/** Normalise a stored member (number | {id,form}) to {id, form}. */
|
||||
export function slot(m) {
|
||||
return typeof m === 'number' ? { id: m, form: null } : { id: m.id, form: m.form || null };
|
||||
}
|
||||
const sameSlot = (a, b) => a.id === b.id && (a.form || null) === (b.form || null);
|
||||
|
||||
export function members() {
|
||||
return team.get().members.map(slot);
|
||||
}
|
||||
|
||||
export function addToTeam(id, form = null) {
|
||||
team.set((s) => {
|
||||
if (s.members.includes(id) || s.members.length >= MAX_TEAM) return s;
|
||||
return { ...s, members: [...s.members, id] };
|
||||
const list = s.members.map(slot);
|
||||
if (list.length >= MAX_TEAM || list.some((m) => sameSlot(m, { id, form }))) return s;
|
||||
return { ...s, members: [...list, { id, form: form || null }] };
|
||||
});
|
||||
}
|
||||
|
||||
export function removeFromTeam(id) {
|
||||
team.set((s) => ({ ...s, members: s.members.filter((x) => x !== id) }));
|
||||
export function removeFromTeam(id, form = null) {
|
||||
team.set((s) => ({
|
||||
...s,
|
||||
members: s.members.map(slot).filter((m) => !sameSlot(m, { id, form: form || null })),
|
||||
}));
|
||||
}
|
||||
|
||||
export function clearTeam() {
|
||||
team.set({ members: [] });
|
||||
}
|
||||
|
||||
export function inTeam(id) {
|
||||
return team.get().members.includes(id);
|
||||
export function inTeam(id, form = null) {
|
||||
return team.get().members.map(slot).some((m) => sameSlot(m, { id, form: form || null }));
|
||||
}
|
||||
|
||||
@ -254,19 +254,23 @@ export async function PokemonDetail(nationalId) {
|
||||
return b;
|
||||
}
|
||||
|
||||
// The active mechanical form (Mega, regional, alt forme) is what gets
|
||||
// added — its baked types/stats drive the Team coverage/compare/calc.
|
||||
const teamForm = () => activeForm?.slug || null;
|
||||
const teamBtn = el('button', {
|
||||
class: 'ptrack__btn ptrack__btn--team',
|
||||
type: 'button',
|
||||
onclick: () => {
|
||||
if (inTeam(nationalId)) removeFromTeam(nationalId);
|
||||
else addToTeam(nationalId);
|
||||
if (inTeam(nationalId, teamForm())) removeFromTeam(nationalId, teamForm());
|
||||
else addToTeam(nationalId, teamForm());
|
||||
syncTeamBtn();
|
||||
},
|
||||
});
|
||||
function syncTeamBtn() {
|
||||
const on = inTeam(nationalId);
|
||||
const on = inTeam(nationalId, teamForm());
|
||||
const full = team.get().members.length >= MAX_TEAM;
|
||||
teamBtn.textContent = on ? '✓ In team' : full ? 'Team full' : '+ Team';
|
||||
const label = activeForm ? `${activeForm.name} in team` : 'In team';
|
||||
teamBtn.textContent = on ? `✓ ${label}` : full ? 'Team full' : activeForm ? `+ ${activeForm.name}` : '+ Team';
|
||||
teamBtn.classList.toggle('is-on', on);
|
||||
teamBtn.disabled = !on && full;
|
||||
}
|
||||
@ -662,6 +666,7 @@ export async function PokemonDetail(nationalId) {
|
||||
activeCosmetic = null;
|
||||
if (cosmeticSelect) cosmeticSelect.value = '';
|
||||
syncFormPills();
|
||||
syncTeamBtn();
|
||||
heroForm.textContent = form ? form.name : '';
|
||||
try {
|
||||
pk = form ? await getPokemon(form.slug) : pokemon;
|
||||
|
||||
@ -2,7 +2,7 @@ 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 { team, addToTeam, removeFromTeam, clearTeam, slot, MAX_TEAM } from '../store/team.js';
|
||||
import { TYPES, defenseVector, multiplier } from '../data/type-chart.js';
|
||||
import { typesForGen } from '../lib/type-resolve.js';
|
||||
import { forGeneration } from '../components/MovesList.js';
|
||||
@ -80,7 +80,28 @@ export async function TeamView() {
|
||||
);
|
||||
|
||||
function members() {
|
||||
return team.get().members.map((id) => snap.speciesById.get(id)).filter(Boolean);
|
||||
return team
|
||||
.get()
|
||||
.members.map((m) => {
|
||||
const s = slot(m);
|
||||
const base = snap.speciesById.get(s.id);
|
||||
if (!base) return null;
|
||||
const f = s.form && (base.forms || []).find((x) => x.slug === s.form);
|
||||
if (!f) return { ...base, _slotForm: null };
|
||||
// Merge the snapshot form's baked types / stats so coverage,
|
||||
// compare and the calc all analyse the form, not the base.
|
||||
return {
|
||||
...base,
|
||||
_slotForm: s.form,
|
||||
name: `${base.name} (${f.name})`,
|
||||
spriteId: f.id,
|
||||
types: f.types || base.types,
|
||||
pastTypes: [],
|
||||
stats: f.stats || base.stats,
|
||||
bst: f.bst ?? base.bst,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function renderLineup() {
|
||||
@ -99,7 +120,7 @@ export async function TeamView() {
|
||||
type: 'button',
|
||||
'aria-label': `Remove ${sp.name}`,
|
||||
onclick: () => {
|
||||
removeFromTeam(sp.id);
|
||||
removeFromTeam(sp.id, sp._slotForm);
|
||||
},
|
||||
},
|
||||
'✕',
|
||||
@ -107,7 +128,7 @@ export async function TeamView() {
|
||||
el(
|
||||
'a',
|
||||
{ href: `#/pokemon/${sp.id}`, class: 'lineup__link' },
|
||||
Sprite(sp.id, { style, alt: sp.name, size: 64 }),
|
||||
Sprite(sp.spriteId ?? sp.id, { style, alt: sp.name, size: 64 }),
|
||||
el('span', { class: 'lineup__name' }, sp.name.replace(/-/g, ' ')),
|
||||
),
|
||||
),
|
||||
@ -120,7 +141,7 @@ export async function TeamView() {
|
||||
{
|
||||
class: 'lineup__add',
|
||||
type: 'button',
|
||||
onclick: () => openPokemonPicker(addToTeam),
|
||||
onclick: () => openPokemonPicker((id) => addToTeam(id), { closeAfterPick: true }),
|
||||
},
|
||||
'+',
|
||||
el('span', {}, 'Add'),
|
||||
@ -215,7 +236,7 @@ export async function TeamView() {
|
||||
href: `#/pokemon/${sp.id}`,
|
||||
title: `${prettify(sp.name)} takes ${m}×`,
|
||||
},
|
||||
Sprite(sp.id, { style, size: 26, alt: sp.name }),
|
||||
Sprite(sp.spriteId ?? sp.id, { style, size: 26, alt: sp.name }),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -234,7 +255,7 @@ export async function TeamView() {
|
||||
el(
|
||||
'a',
|
||||
{ class: 'cov__mem', href: `#/pokemon/${sp.id}`, title: prettify(sp.name) },
|
||||
Sprite(sp.id, { style, size: 34, alt: sp.name }),
|
||||
Sprite(sp.spriteId ?? sp.id, { style, size: 34, alt: sp.name }),
|
||||
),
|
||||
),
|
||||
el('span', { class: 'cov__bal cov__bal--head' }, 'resist · weak'),
|
||||
@ -410,7 +431,7 @@ export async function TeamView() {
|
||||
? el(
|
||||
'button',
|
||||
{ type: 'button', class: 'calc__pick', onclick: () => pick() },
|
||||
Sprite(sp.id, { style, size: 48, alt: sp.name }),
|
||||
Sprite(sp.spriteId ?? sp.id, { style, size: 48, alt: sp.name }),
|
||||
el('span', {}, prettify(sp.name)),
|
||||
)
|
||||
: el('button', { type: 'button', class: 'calc__pick calc__pick--empty', onclick: () => pick() }, `Choose ${sideLabel}…`),
|
||||
@ -441,7 +462,7 @@ export async function TeamView() {
|
||||
recalc();
|
||||
},
|
||||
},
|
||||
Sprite(sp.id, { style, size: 26, alt: sp.name }),
|
||||
Sprite(sp.spriteId ?? sp.id, { style, size: 26, alt: sp.name }),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user