dex/src/store/selection.js
chris 17e8e30f9b Import the Pokédex from a Gen 3 GBA save (.sav / .srm)
lib/savedex.js parses a Generation III save (Ruby/Sapphire/Emerald/
FireRed/LeafGreen): picks the newer of the two save slots, resolves the
rotated section order via each section's footer, and reads the seen /
owned bitfields out of `struct Pokedex` in section 0 (bit dexNo-1, 386
species). Also decodes the trainer name and detects the national-dex
magic byte. Soft checksum check.

Settings > Your data: "Import from a GBA save" — file picker → confirm →
selection.importFlags() merges seen/caught in (never removes). Verified
against synthetic saves (slot selection, rotation, FRLG vs RSE, bit
decode) and end-to-end through the Settings UI.

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

76 lines
2.2 KiB
JavaScript

import { createStore } from './createStore.js';
/**
* The "unified Pokémon selection".
*
* One record per Pokémon, keyed by NATIONAL dex id. That key is what makes
* it unified: mark Pikachu once and every game's view — Kanto, Paldea,
* National, search results — reflects it, because each view computes its
* own progress by intersecting its species list with this single map.
*/
export const selection = createStore('pdx.selection', {
version: 2,
pokemon: {},
});
const BLANK = { seen: false, caught: false, favorite: false, note: '' };
export function entry(id) {
return selection.get().pokemon[id] || BLANK;
}
export function toggle(id, field) {
selection.set((s) => {
const current = s.pokemon[id] || BLANK;
const next = {
...current,
[field]: !current[field],
updatedAt: new Date().toISOString(),
};
// Catching something implies you've seen it.
if (field === 'caught' && next.caught) next.seen = true;
return { ...s, pokemon: { ...s.pokemon, [id]: next } };
});
}
export function setNote(id, note) {
selection.set((s) => {
const current = s.pokemon[id] || BLANK;
return {
...s,
pokemon: {
...s.pokemon,
[id]: { ...current, note, updatedAt: new Date().toISOString() },
},
};
});
}
/** Merge in seen/caught species lists (e.g. from a save-file import). Never clears. */
export function importFlags({ seen = [], caught = [] } = {}) {
selection.set((s) => {
const pokemon = { ...s.pokemon };
const stamp = new Date().toISOString();
const bump = (id, patch) => {
pokemon[id] = { ...(pokemon[id] || BLANK), ...patch, updatedAt: stamp };
};
for (const id of seen) bump(id, { seen: true });
for (const id of caught) bump(id, { seen: true, caught: true });
return { ...s, pokemon };
});
}
/** Aggregate seen/caught counts over an arbitrary list of national ids. */
export function stats(speciesIds) {
const p = selection.get().pokemon;
let seen = 0;
let caught = 0;
for (const id of speciesIds) {
const e = p[id];
if (!e) continue;
if (e.seen || e.caught) seen++;
if (e.caught) caught++;
}
return { seen, caught, total: speciesIds.length };
}