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
322 lines
16 KiB
Markdown
322 lines
16 KiB
Markdown
# Pocketdex
|
||
|
||
An offline-first **Pokédex, team builder and save-file tracker**, built on
|
||
[PokéAPI](https://pokeapi.co/). Vanilla JS + [Vite](https://vitejs.dev/) +
|
||
[Workbox](https://developer.chrome.com/docs/workbox) — no UI framework, no
|
||
runtime dependencies in the app bundle.
|
||
|
||
- **Pick a game** and everything reshapes to it — regional numbering,
|
||
era-accurate typings, that generation's type chart and move data, the
|
||
right flavour text.
|
||
- **Track seen/caught per game.** Catching Pikachu in Blue says nothing
|
||
about your Gold save. `#/progress` shows each game's own tally.
|
||
- **Import a real save file** (`.sav` / `.srm` / `.dsv`), Gen 1–5, and read
|
||
the Pokédex straight out of it.
|
||
- **Works offline** after the first load, and installs as a PWA.
|
||
|
||
> **Not affiliated with Nintendo, Game Freak or The Pokémon Company.** This
|
||
> is a non-commercial fan reference tool. See [Legal](#legal).
|
||
|
||
<p align="center">
|
||
<img src="docs/screenshots/dex-grid.png" alt="Dex grid" width="24%">
|
||
<img src="docs/screenshots/detail.png" alt="Pokémon detail" width="24%">
|
||
<img src="docs/screenshots/team-coverage.png" alt="Team coverage analysis" width="24%">
|
||
<img src="docs/screenshots/progress.png" alt="Per-game progress" width="24%">
|
||
</p>
|
||
|
||
---
|
||
|
||
## Quick start
|
||
|
||
Requires **Node 20+**.
|
||
|
||
```bash
|
||
git clone https://github.com/OWNER/pocketdex.git
|
||
cd pocketdex
|
||
npm install
|
||
npm run snapshot # fetch the data snapshot from PokéAPI (one time)
|
||
npm run dev # http://localhost:5173
|
||
```
|
||
|
||
| Script | What it does |
|
||
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `npm run snapshot` | Build `src/data/snapshot.json` from PokéAPI. Skips if the file is under 30 days old; `npm run snapshot -- --force` to rebuild anyway. |
|
||
| `npm run dev` | Vite dev server with HMR. |
|
||
| `npm run build` | Runs `snapshot` (as `prebuild`), then builds to `dist/`. |
|
||
| `npm run preview` | Serve the production build. **Use this to test the service worker / offline / install** — none of that runs under `npm run dev`. |
|
||
|
||
The snapshot is **git-ignored** — it's a build artifact. `npm install`
|
||
doesn't create it; run `npm run snapshot` (or `npm run build`) first, or
|
||
the app has no data to show.
|
||
|
||
---
|
||
|
||
## Features
|
||
|
||
### Dex grid
|
||
|
||
Type-tinted cards — official artwork over a type-coloured spotlight, ghost
|
||
number, per-dex progress ring. Filter by name/number; chips for
|
||
caught / missing / favourites / legendary / has-a-note; a filter drawer for
|
||
type, generation, ability, egg group, minimum BST and "fully evolved only"
|
||
(all derived client-side from the snapshot — no extra fetches). Sort by dex
|
||
number, name, any base stat, BST, height, weight, base EXP, catch rate or
|
||
"recently caught". When a specific game is selected, cards show a
|
||
version-exclusive badge ("Ruby only"), derived from baked wild-encounter
|
||
data (so gift/trade-only exclusives are missed).
|
||
|
||
### Detail page
|
||
|
||
Type-gradient hero with a shared-element morph from the tapped card, then a
|
||
tabbed sheet:
|
||
|
||
- **About** — flavour text for the selected game (with an "all entries"
|
||
disclosure), physical data, abilities (each linking to its own page),
|
||
held items, egg groups, gender rate, catch rate, growth rate.
|
||
- **Stats** — colour-coded animated bars with an animated total.
|
||
- **Evolution** — the chain re-parented to the selected game's generation.
|
||
- **Moves** — the full learnset with per-move power / type / damage class /
|
||
accuracy / PP / effect, **era-accurate** via PokéAPI `past_values`
|
||
(including pre-Gen-4 physical/special-by-type). The TM/HM group is ordered
|
||
by machine number for the selected game.
|
||
- **Locations** — wild encounter locations for the selected game.
|
||
|
||
Forms (Mega, Gigantamax, regional, alternate formes) get a pill switcher
|
||
that rebuilds types / stats / matchups / abilities / learnset / artwork.
|
||
Purely cosmetic variants — Unown's letters, Vivillon's patterns, Alcremie's
|
||
decorations, Pikachu caps, seasonal Deerling, and so on — get a separate
|
||
compact **Appearance** dropdown, plus a per-species "form dex" checklist
|
||
tracked independently of the main caught flag.
|
||
|
||
### Team builder (`#/team`)
|
||
|
||
A lineup of up to 6 (specific forms included). Three modes:
|
||
|
||
- **Coverage** — leads with a _weak spots_ summary (types that hit 2+
|
||
members, or that someone's weak to and nobody resists), then a full
|
||
defensive matchup grid with a diverging resist ◀│▶ weak bar per type,
|
||
offensive STAB gaps, and at-a-glance stats.
|
||
- **Compare** — the six stacked side by side. (The same table is also a
|
||
standalone `#/compare` view with its own picker, up to 4 Pokémon.)
|
||
- **Calc** — a damage calculator. Attacker + move + defender → damage range,
|
||
% of HP, hits-to-KO, STAB/effectiveness badges, with level / nature / EV
|
||
controls. It's a base-stats estimate: 31 IVs assumed, no
|
||
items / abilities / weather / terrain.
|
||
|
||
All three follow the selected game — its generation's type chart,
|
||
era-accurate typings (pre-Gen-6 Clefairy is Normal, etc.) and era move
|
||
data. Links out to a **Natures** table, an interactive **Type chart**
|
||
(`#/types` — tap a type for its offensive + defensive breakdown, gen-aware)
|
||
and a **Breeding** helper (`#/breeding` — egg-group browser, compatibility
|
||
check, and egg moves resolved to the base breeding stage).
|
||
|
||
### Search (`#/search`)
|
||
|
||
Tabbed lookup — Pokémon, Moves, Items, Abilities — all browsable offline
|
||
from the snapshot. Moves filter by type / damage class / "TMs only" /
|
||
"HMs only" for the selected game and sort by power / accuracy / recency, or
|
||
by TM/HM number when a machine filter is on. Items filter by category. The
|
||
move, ability and item detail pages each list the Pokémon connected to them
|
||
(learners / users / wild holders) as a filterable grid, narrowed to the
|
||
selected game. Query, scroll, tab and filters persist.
|
||
|
||
### Progress by game (`#/progress`)
|
||
|
||
Every game's seen/caught tally against its own dex, from its own per-game
|
||
bucket. Leads with the games you've played (selected at least once, or
|
||
imported a save for); the rest sit behind a toggle. The National Dex row is
|
||
the union across all games. **Edit** the "Your games" list to reset a
|
||
game's caught/seen or drop it from the list; **Reset caught / seen…** wipes
|
||
every game at once (favorites and notes are kept — only Settings →
|
||
_Clear tracking data_ removes those).
|
||
|
||
### Shiny hunt tracker (`#/shiny`)
|
||
|
||
Per-hunt counters with method-aware odds (full odds, Masuda, SOS, chain
|
||
fishing, DexNav, radar, dynamax adventures, outbreaks, mass outbreaks) and
|
||
Shiny Charm, cumulative-probability readout, notes, and a "found it" that
|
||
marks the Pokémon caught.
|
||
|
||
### Save-file import
|
||
|
||
**Settings → Import from a game save.** Reads the game's own seen/owned
|
||
bitfields — not an approximation — from:
|
||
|
||
| Gen | Games | Format |
|
||
| --- | -------------------------------------------------- | ----------------------------------------------------------------------- |
|
||
| 1 | Red / Blue / Yellow | 32 KB SRAM (`.sav` / `.srm`) |
|
||
| 2 | Gold / Silver / Crystal | 32 KB SRAM — checksum picks G/S vs Crystal; also reads the Unown Report |
|
||
| 3 | Ruby / Sapphire / Emerald, FireRed / LeafGreen | 128 KB — newer slot, rotated sections |
|
||
| 4 | Diamond / Pearl / Platinum, HeartGold / SoulSilver | 512 KB NDS — active slot, DeSmuME `.dsv` footer stripped |
|
||
| 5 | Black / White, Black 2 / White 2 | 512 KB NDS — per-form "seen" copies OR'd |
|
||
|
||
Gen 1–3 are checksum-verified; Gen 4–5 offsets come from PKHeX and are
|
||
validated structurally (nothing set past the last species; every caught
|
||
species is also seen). A dialog then offers **merge** or **replace** into
|
||
one game's bucket — the selected game if the save could be it, otherwise
|
||
the parser's best guess.
|
||
|
||
The dex bitfields are per-species, so form sets (Unown letters, Vivillon
|
||
patterns…) can't be recovered from them — **except** the Gen 2 Unown
|
||
Report, which records the letters you've caught; a Gen 2 import ticks
|
||
those in the per-letter Form Dex.
|
||
|
||
Full local state (settings, per-game tracking, team, form tracking, shiny
|
||
hunts, played games) also exports / imports as a single JSON file.
|
||
|
||
### Settings
|
||
|
||
Theme (System / Light / Dark / Black OLED / Sepia) + accent colour; text
|
||
size; an in-app reduce-motion override (the OS preference is always
|
||
respected too); sprite style (modern pixel / game-era pixel / official
|
||
artwork / HOME); haptic feedback on catch (Vibration API); which screen to
|
||
land on at launch; JSON export / import; cache and tracking resets.
|
||
|
||
### PWA
|
||
|
||
Workbox service worker: precache the app shell + snapshot; stale-while-
|
||
revalidate for PokéAPI JSON (30-day TTL); cache-first LRU for sprites;
|
||
in-app update toast. Installable with maskable icons, `beforeinstallprompt`
|
||
captured for an in-app offer, and manifest shortcuts to Team / Search /
|
||
Type chart.
|
||
|
||
---
|
||
|
||
## How it works
|
||
|
||
### Data & storage
|
||
|
||
| Layer | Where | Notes |
|
||
| ---------------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| Preferences + tracking | `localStorage` (`pdx.*` keys) | Tiny, synchronous, restored on reload. |
|
||
| Build-time snapshot | `src/data/snapshot.json` (~150 KB) | Every species (id, name, generation, current + past typings, base stats, height/weight, species flags), every regional Pokédex, every version group, plus move / item / ability name indexes and baked TM numbers and version-exclusivity. Precached by the SW. |
|
||
| Detail data | `pokeapi.co`, fetched lazily per page | Stats, abilities, flavour text, evolution, encounters. Cached by the SW (stale-while-revalidate). |
|
||
| Sprites | PokéAPI sprites repo, cache-first | Capped LRU. |
|
||
|
||
The list, search, sort, filters and game switching run **entirely off the
|
||
snapshot** — no network until you open a detail page.
|
||
|
||
### Per-game tracking
|
||
|
||
`src/store/selection.js`:
|
||
|
||
```
|
||
pokemon: { [id]: { favorite, note } } // global — describe the Pokémon
|
||
games: { [vgKey]: { [id]: { seen, caught } } } // per game
|
||
```
|
||
|
||
`entry(id)` / `toggle(id, field)` / `stats(ids)` default to the selected
|
||
game; `favorite` and `note` are always global. The `all` key ("All games"
|
||
mode) reads as the **union** of every game — and is where an older
|
||
single-record store or JSON backup migrates to (`normalizeSelection`), so
|
||
nothing is falsely attributed to a game you never played.
|
||
|
||
### Routing & rendering
|
||
|
||
Hash router (`src/router.js`) over a small route table. Each view is an
|
||
`async` function that returns a DOM node; the router swaps it into `#view`
|
||
using the View Transitions API when available (and when the user hasn't
|
||
asked for reduced motion). Detail routes get a shaped skeleton first so the
|
||
tapped card's sprite can morph into the hero.
|
||
|
||
No framework. DOM is built with a ~30-line `el()` helper
|
||
(`src/lib/dom.js`). State is a handful of `localStorage`-backed reactive
|
||
stores from `createStore()` — `get()`, `set(patch | fn)`,
|
||
`subscribe(fn) → unsub`, `replace(next)`.
|
||
|
||
### Project layout
|
||
|
||
```
|
||
scripts/build-snapshot.mjs PokéAPI → snapshot.json
|
||
src/
|
||
main.js boot: theme, nav, router, SW registration, install prompt
|
||
router.js hash router + View Transitions
|
||
sw.js Workbox service worker (injectManifest)
|
||
store/ createStore + settings / selection / team / ui / …
|
||
data/ snapshot loader, pokedex resolver, lazy API client,
|
||
type chart, natures, game colours
|
||
lib/ dom, anim, damage-calc, savedex, swipe, dialog, haptics, …
|
||
components/ Card, Sprite, TypeChip, StatBar, CompareTable, Nav, …
|
||
views/ one file per route (DexGrid, PokemonDetail, TeamView, …)
|
||
styles/ tokens.css (palette, type colours, themes), layout.css
|
||
```
|
||
|
||
---
|
||
|
||
## Deployment
|
||
|
||
Static output — host `dist/` anywhere.
|
||
|
||
**Sub-path** (e.g. a GitHub Pages project site at `/pocketdex/`):
|
||
|
||
```bash
|
||
BASE_PATH=/pocketdex/ npm run build
|
||
```
|
||
|
||
**Docker** (multi-stage Node → nginx):
|
||
|
||
```bash
|
||
docker compose up --build # → http://localhost:4753
|
||
# or
|
||
docker build -t pocketdex .
|
||
docker run --rm -p 4753:80 pocketdex
|
||
```
|
||
|
||
The build stage's `prebuild` fetches the snapshot, so `docker build` needs
|
||
network access to `pokeapi.co` unless a fresh `src/data/snapshot.json` is
|
||
already present (it's copied in and reused). Sub-path:
|
||
`docker build --build-arg BASE_PATH=/pocketdex/ .`. `nginx.conf` sets the
|
||
SPA fallback and long-cache headers for `/assets/*` while keeping
|
||
`index.html` / `sw.js` uncached.
|
||
|
||
---
|
||
|
||
## Contributing
|
||
|
||
Issues and pull requests are welcome — see
|
||
**[CONTRIBUTING.md](CONTRIBUTING.md)** for setup, project conventions, and
|
||
what's in and out of scope. AI coding agents: there's an
|
||
**[AGENTS.md](AGENTS.md)**.
|
||
|
||
Known gaps: no interactive region map (PokéAPI has no map imagery or
|
||
coordinates), a thin unit-test suite (pure logic only — no UI/integration
|
||
tests), and no formal Lighthouse pass.
|
||
|
||
---
|
||
|
||
## Built with AI assistance
|
||
|
||
Pocketdex is developed with heavy use of AI coding tools — primarily
|
||
Anthropic's Claude, via Claude Code. Direction, design decisions, review
|
||
and testing are human-led; a large share of the implementation is
|
||
AI-generated and then reviewed before it lands. You'll see this in the git
|
||
history (`Co-Authored-By: Claude …` trailers).
|
||
|
||
This doesn't change anything for contributors: use whatever tools you like,
|
||
disclose AI-generated PRs in the description, and expect the same review
|
||
either way. There's an [AGENTS.md](AGENTS.md) so agents have the house
|
||
rules too.
|
||
|
||
---
|
||
|
||
## Legal
|
||
|
||
Pocketdex is an unofficial, non-commercial fan project. It is **not
|
||
affiliated with, endorsed by, or sponsored by** Nintendo, Game Freak, The
|
||
Pokémon Company, or PokéAPI.
|
||
|
||
- Pokémon and all related names, sprites and artwork are **© Nintendo /
|
||
Game Freak / The Pokémon Company**. They are served at runtime from
|
||
PokéAPI and its sprite repository; **none are committed to this
|
||
repository**, and none should be added in a pull request.
|
||
- Game data is provided by [PokéAPI](https://pokeapi.co/) under its terms.
|
||
- The build-time snapshot (`snapshot.json`) is generated locally from
|
||
PokéAPI and is git-ignored.
|
||
|
||
If you represent a rights holder and have a concern, please open an issue.
|
||
|
||
## License
|
||
|
||
[MIT](LICENSE) — applies to the Pocketdex source code only, not to any
|
||
Pokémon data or assets it displays.
|