# 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).
---
## 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.
### 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 |
| 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.
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.