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
76 lines
2.2 KiB
JavaScript
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 };
|
|
}
|