6 Commits

Author SHA1 Message Date
bbc4cb48ca Save import: read the Gen 2 Unown Report
Gen 2 (GSC) is the one mainline dex that records which Unown letters
you've caught — a 26-byte ordered list right after the Seen flag array
(seen + 0x20). Parse it, map letters to the app's Form Dex slugs
('unown' for A, 'unown-b'…'unown-z'), and tick them via a new
markFormsCaught() on the formTracking store. A garbage/out-of-range list
is ignored rather than guessed at.

The import dialog and result line now surface "· N Unown letters".

Verified: 6 new unit tests (synthetic Gen 2 SRAM → slugs, empty list,
garbage rejection; markFormsCaught behaviour) + a real end-to-end import
of a synthetic Crystal save writing {unown, unown-c, unown-o, unown-z}
to pdx.formTracking.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
2026-09-10 12:21:41 -04:00
671e72972a Apply Prettier to the whole tree
Pure formatting — no behaviour change. Verified: build passes, all 14
routes render, service worker active, no console errors.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
2026-09-10 11:24:31 -04:00
a70f3c9976 Team: chip-style tool links; Progress: lead with games you've played
The Natures / Type chart / Compare / Breeding links under the Team header
were plain dot-separated text. Make them a row of pill chips with small
accent icon badges (.toollinks / .toollink).

Progress by game showed a caught total for all 32 version groups the
moment anything was marked, since the tracking model is one unified
caught record. Add a "played games" set (pdx.playedGames, vg keys),
auto-marked when a game is selected or a save is imported (savedex now
returns candidate versionGroups per generation). #/progress leads with a
"Your games" section and tucks the rest behind a "Show all other games"
toggle (persisted). Included in JSON export/import.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
2026-09-10 10:34:20 -04:00
c1ce972ce9 Save import: add Gen 4 and Gen 5 (NDS)
savedex.js now also reads 512 KB DS saves:

- Gen 4 (D/P/Pt, HG/SS): dex struct at slot+0x12 — caught 0x40 bytes at
  +0x16, four per-form "seen" copies at +0x56/0x96/0xD6/0x116 OR'd
  together, 493 species.
- Gen 5 (B/W, B2/W2): dex block at slot+0x21600 (BW) / +0x21400 (B2W2) —
  caught at +0x08, four "seen" copies at +0x5C/0xB0/0x104/0x158, 649
  species; the offset that validates picks the game.
- Handles both save slots (picks the fuller one), a DeSmuME `.dsv`
  footer, and oversized flash dumps.

No block CRC — validated structurally instead (nothing set past the last
species, every caught species also seen), so a wrong offset is rejected
rather than importing garbage. Offsets are from PKHeX; worth checking
against a real save.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
2026-09-10 10:10:56 -04:00
78df1a32e8 Save import: add Gen 1 and Gen 2
savedex.js now sniffs the file and parses whichever generation it is:

- Gen 1 (R/B/Y): 32 KB SRAM, fixed offsets — owned 0x25A3, seen 0x25B6
  (151 species), name 0x2598, one-byte complement checksum.
- Gen 2 (G/S/C): tries Gold/Silver offsets (0x2A4C/0x2A6C, checksum
  0x2D69) and Crystal (0x2A27/0x2A47, checksum 0x2D0D); the 16-bit
  checksum picks which, with a caught⊆seen / no-overflow sanity fallback.
- Gen 3: unchanged (parseGen3Dex kept as an alias).

parseSaveDex(buffer) is the new entry point; the Settings picker relabels
to "Import from a game save". Verified against synthetic Gen 1 / GS /
Crystal saves and end-to-end through the UI (a Gen 1 save merged
11 caught / 84 seen).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
2026-09-10 09:44:38 -04:00
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