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
This commit is contained in:
parent
dfd848d506
commit
671e72972a
18
.github/ISSUE_TEMPLATE/bug_report.md
vendored
18
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,24 +1,24 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Something isn't working right
|
about: Something isn't working right
|
||||||
title: ""
|
title: ''
|
||||||
labels: bug
|
labels: bug
|
||||||
assignees: ""
|
assignees: ''
|
||||||
---
|
---
|
||||||
|
|
||||||
**What happened**
|
**What happened** (and what you expected instead)
|
||||||
<!-- and what you expected instead -->
|
|
||||||
|
|
||||||
**Steps to reproduce**
|
**Steps to reproduce**
|
||||||
1.
|
|
||||||
2.
|
1. Go to …
|
||||||
3.
|
2. …
|
||||||
|
3. …
|
||||||
|
|
||||||
**Context**
|
**Context**
|
||||||
|
|
||||||
- Browser / OS:
|
- Browser / OS:
|
||||||
- Installed as a PWA or in a browser tab:
|
- Installed as a PWA or in a browser tab:
|
||||||
- Selected game (if relevant):
|
- Selected game (if relevant):
|
||||||
- Console errors (DevTools → Console):
|
- Console errors (DevTools → Console):
|
||||||
|
|
||||||
**Screenshot / clip**
|
**Screenshot / clip** (optional but very helpful)
|
||||||
<!-- optional but very helpful -->
|
|
||||||
|
|||||||
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: Feature request
|
name: Feature request
|
||||||
about: Suggest an idea
|
about: Suggest an idea
|
||||||
title: ""
|
title: ''
|
||||||
labels: enhancement
|
labels: enhancement
|
||||||
assignees: ""
|
assignees: ''
|
||||||
---
|
---
|
||||||
|
|
||||||
**The idea**
|
**The idea**
|
||||||
|
|||||||
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@ -1,18 +1,18 @@
|
|||||||
version: 2
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: npm
|
- package-ecosystem: npm
|
||||||
directory: "/"
|
directory: '/'
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: weekly
|
||||||
groups:
|
groups:
|
||||||
workbox:
|
workbox:
|
||||||
patterns:
|
patterns:
|
||||||
- "workbox-*"
|
- 'workbox-*'
|
||||||
dev-dependencies:
|
dev-dependencies:
|
||||||
dependency-type: development
|
dependency-type: development
|
||||||
open-pull-requests-limit: 5
|
open-pull-requests-limit: 5
|
||||||
|
|
||||||
- package-ecosystem: github-actions
|
- package-ecosystem: github-actions
|
||||||
directory: "/"
|
directory: '/'
|
||||||
schedule:
|
schedule:
|
||||||
interval: monthly
|
interval: monthly
|
||||||
|
|||||||
26
AGENTS.md
26
AGENTS.md
@ -28,18 +28,18 @@ npm run preview # serve the production build — REQUIRED to test the se
|
|||||||
|
|
||||||
## Project structure
|
## Project structure
|
||||||
|
|
||||||
| Path | What |
|
| Path | What |
|
||||||
| --- | --- |
|
| ---------------------------- | --------------------------------------------------------------------------- |
|
||||||
| `src/main.js` | boot: theme, nav, router, SW registration, install prompt |
|
| `src/main.js` | boot: theme, nav, router, SW registration, install prompt |
|
||||||
| `src/router.js` | hash router; `routes` array; each view is `() => Promise<Node>` |
|
| `src/router.js` | hash router; `routes` array; each view is `() => Promise<Node>` |
|
||||||
| `src/views/*.js` | one file per route (`DexGrid`, `PokemonDetail`, `TeamView`, …) |
|
| `src/views/*.js` | one file per route (`DexGrid`, `PokemonDetail`, `TeamView`, …) |
|
||||||
| `src/components/*.js` | reusable DOM pieces (`Card`, `Sprite`, `TypeChip`, `CompareTable`, …) |
|
| `src/components/*.js` | reusable DOM pieces (`Card`, `Sprite`, `TypeChip`, `CompareTable`, …) |
|
||||||
| `src/store/*.js` | `createStore()` state, persisted to `localStorage` under `pdx.*` |
|
| `src/store/*.js` | `createStore()` state, persisted to `localStorage` under `pdx.*` |
|
||||||
| `src/data/*.js` | snapshot loader, pokedex resolver, lazy PokéAPI client, type chart, natures |
|
| `src/data/*.js` | snapshot loader, pokedex resolver, lazy PokéAPI client, type chart, natures |
|
||||||
| `src/lib/*.js` | app-agnostic helpers (`dom`, `anim`, `damage-calc`, `savedex`, `swipe`, …) |
|
| `src/lib/*.js` | app-agnostic helpers (`dom`, `anim`, `damage-calc`, `savedex`, `swipe`, …) |
|
||||||
| `scripts/build-snapshot.mjs` | the only thing that calls PokéAPI at build time |
|
| `scripts/build-snapshot.mjs` | the only thing that calls PokéAPI at build time |
|
||||||
| `src/styles/tokens.css` | palette, type colours, the five themes |
|
| `src/styles/tokens.css` | palette, type colours, the five themes |
|
||||||
| `src/styles/layout.css` | everything else |
|
| `src/styles/layout.css` | everything else |
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
@ -61,7 +61,7 @@ npm run preview # serve the production build — REQUIRED to test the se
|
|||||||
- Respect `prefersReducedMotion()` (from `src/store/settings.js`) for any
|
- Respect `prefersReducedMotion()` (from `src/store/settings.js`) for any
|
||||||
animation.
|
animation.
|
||||||
- 2-space indent, semicolons, single quotes, trailing commas in multi-line
|
- 2-space indent, semicolons, single quotes, trailing commas in multi-line
|
||||||
literals. Match the surrounding file; keep comments about *why*.
|
literals. Match the surrounding file; keep comments about _why_.
|
||||||
- No new runtime dependencies. Prefer adding to the snapshot over a new
|
- No new runtime dependencies. Prefer adding to the snapshot over a new
|
||||||
runtime fetch.
|
runtime fetch.
|
||||||
|
|
||||||
|
|||||||
@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
|
|||||||
Examples of behavior that contributes to a positive environment for our
|
Examples of behavior that contributes to a positive environment for our
|
||||||
community include:
|
community include:
|
||||||
|
|
||||||
* Demonstrating empathy and kindness toward other people
|
- Demonstrating empathy and kindness toward other people
|
||||||
* Being respectful of differing opinions, viewpoints, and experiences
|
- Being respectful of differing opinions, viewpoints, and experiences
|
||||||
* Giving and gracefully accepting constructive feedback
|
- Giving and gracefully accepting constructive feedback
|
||||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
and learning from the experience
|
and learning from the experience
|
||||||
* Focusing on what is best not just for us as individuals, but for the overall
|
- Focusing on what is best not just for us as individuals, but for the overall
|
||||||
community
|
community
|
||||||
|
|
||||||
Examples of unacceptable behavior include:
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
- The use of sexualized language or imagery, and sexual attention or advances of
|
||||||
any kind
|
any kind
|
||||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
* Public or private harassment
|
- Public or private harassment
|
||||||
* Publishing others' private information, such as a physical or email address,
|
- Publishing others' private information, such as a physical or email address,
|
||||||
without their explicit permission
|
without their explicit permission
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
- Other conduct which could reasonably be considered inappropriate in a
|
||||||
professional setting
|
professional setting
|
||||||
|
|
||||||
## Enforcement Responsibilities
|
## Enforcement Responsibilities
|
||||||
|
|||||||
@ -65,7 +65,7 @@ code**:
|
|||||||
- Stores: `set(fn)` **replaces** state — spread `...s` yourself. Read with
|
- Stores: `set(fn)` **replaces** state — spread `...s` yourself. Read with
|
||||||
`get()`, react with `subscribe()`, and unsubscribe in the view's
|
`get()`, react with `subscribe()`, and unsubscribe in the view's
|
||||||
teardown (`onTeardown(view, off)`).
|
teardown (`onTeardown(view, off)`).
|
||||||
- Keep comments about *why*, not *what*. Match the existing density.
|
- Keep comments about _why_, not _what_. Match the existing density.
|
||||||
- Respect `prefersReducedMotion()` for any new animation.
|
- Respect `prefersReducedMotion()` for any new animation.
|
||||||
- Keep everything game-aware: if you touch typings, type effectiveness,
|
- Keep everything game-aware: if you touch typings, type effectiveness,
|
||||||
learnsets or evolution, honour the selected game's generation
|
learnsets or evolution, honour the selected game's generation
|
||||||
@ -105,7 +105,7 @@ Describe what you tested in the PR.
|
|||||||
|
|
||||||
- Branch off `main`. One focused change per PR.
|
- Branch off `main`. One focused change per PR.
|
||||||
- Imperative commit subjects ("Add …", "Fix …"), with a body explaining
|
- Imperative commit subjects ("Add …", "Fix …"), with a body explaining
|
||||||
the *why* when it isn't obvious.
|
the _why_ when it isn't obvious.
|
||||||
- Don't commit `dist/` or `src/data/snapshot.json` (both git-ignored).
|
- Don't commit `dist/` or `src/data/snapshot.json` (both git-ignored).
|
||||||
- Fill in the PR template. Screenshots or a short clip for anything
|
- Fill in the PR template. Screenshots or a short clip for anything
|
||||||
visual.
|
visual.
|
||||||
|
|||||||
47
README.md
47
README.md
@ -38,12 +38,12 @@ npm run snapshot # fetch the data snapshot from PokéAPI (one time)
|
|||||||
npm run dev # http://localhost:5173
|
npm run dev # http://localhost:5173
|
||||||
```
|
```
|
||||||
|
|
||||||
| Script | What it does |
|
| 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 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 dev` | Vite dev server with HMR. |
|
||||||
| `npm run build` | Runs `snapshot` (as `prebuild`), then builds to `dist/`. |
|
| `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`. |
|
| `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`
|
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
|
doesn't create it; run `npm run snapshot` (or `npm run build`) first, or
|
||||||
@ -54,6 +54,7 @@ the app has no data to show.
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
### Dex grid
|
### Dex grid
|
||||||
|
|
||||||
Type-tinted cards — official artwork over a type-coloured spotlight, ghost
|
Type-tinted cards — official artwork over a type-coloured spotlight, ghost
|
||||||
number, per-dex progress ring. Filter by name/number; chips for
|
number, per-dex progress ring. Filter by name/number; chips for
|
||||||
caught / missing / favourites / legendary / has-a-note; a filter drawer for
|
caught / missing / favourites / legendary / has-a-note; a filter drawer for
|
||||||
@ -65,6 +66,7 @@ version-exclusive badge ("Ruby only"), derived from baked wild-encounter
|
|||||||
data (so gift/trade-only exclusives are missed).
|
data (so gift/trade-only exclusives are missed).
|
||||||
|
|
||||||
### Detail page
|
### Detail page
|
||||||
|
|
||||||
Type-gradient hero with a shared-element morph from the tapped card, then a
|
Type-gradient hero with a shared-element morph from the tapped card, then a
|
||||||
tabbed sheet:
|
tabbed sheet:
|
||||||
|
|
||||||
@ -87,9 +89,10 @@ compact **Appearance** dropdown, plus a per-species "form dex" checklist
|
|||||||
tracked independently of the main caught flag.
|
tracked independently of the main caught flag.
|
||||||
|
|
||||||
### Team builder (`#/team`)
|
### Team builder (`#/team`)
|
||||||
|
|
||||||
A lineup of up to 6 (specific forms included). Three modes:
|
A lineup of up to 6 (specific forms included). Three modes:
|
||||||
|
|
||||||
- **Coverage** — leads with a *weak spots* summary (types that hit 2+
|
- **Coverage** — leads with a _weak spots_ summary (types that hit 2+
|
||||||
members, or that someone's weak to and nobody resists), then a full
|
members, or that someone's weak to and nobody resists), then a full
|
||||||
defensive matchup grid with a diverging resist ◀│▶ weak bar per type,
|
defensive matchup grid with a diverging resist ◀│▶ weak bar per type,
|
||||||
offensive STAB gaps, and at-a-glance stats.
|
offensive STAB gaps, and at-a-glance stats.
|
||||||
@ -108,6 +111,7 @@ and a **Breeding** helper (`#/breeding` — egg-group browser, compatibility
|
|||||||
check, and egg moves resolved to the base breeding stage).
|
check, and egg moves resolved to the base breeding stage).
|
||||||
|
|
||||||
### Search (`#/search`)
|
### Search (`#/search`)
|
||||||
|
|
||||||
Tabbed lookup — Pokémon, Moves, Items, Abilities — all browsable offline
|
Tabbed lookup — Pokémon, Moves, Items, Abilities — all browsable offline
|
||||||
from the snapshot. Moves filter by type / damage class / "TMs only" /
|
from the snapshot. Moves filter by type / damage class / "TMs only" /
|
||||||
"HMs only" for the selected game and sort by power / accuracy / recency, or
|
"HMs only" for the selected game and sort by power / accuracy / recency, or
|
||||||
@ -117,28 +121,31 @@ move, ability and item detail pages each list the Pokémon connected to them
|
|||||||
selected game. Query, scroll, tab and filters persist.
|
selected game. Query, scroll, tab and filters persist.
|
||||||
|
|
||||||
### Progress by game (`#/progress`)
|
### Progress by game (`#/progress`)
|
||||||
|
|
||||||
Every game's seen/caught tally against its own dex, from its own per-game
|
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
|
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
|
imported a save for); the rest sit behind a toggle. The National Dex row is
|
||||||
the union across all games.
|
the union across all games.
|
||||||
|
|
||||||
### Shiny hunt tracker (`#/shiny`)
|
### Shiny hunt tracker (`#/shiny`)
|
||||||
|
|
||||||
Per-hunt counters with method-aware odds (full odds, Masuda, SOS, chain
|
Per-hunt counters with method-aware odds (full odds, Masuda, SOS, chain
|
||||||
fishing, DexNav, radar, dynamax adventures, outbreaks, mass outbreaks) and
|
fishing, DexNav, radar, dynamax adventures, outbreaks, mass outbreaks) and
|
||||||
Shiny Charm, cumulative-probability readout, notes, and a "found it" that
|
Shiny Charm, cumulative-probability readout, notes, and a "found it" that
|
||||||
marks the Pokémon caught.
|
marks the Pokémon caught.
|
||||||
|
|
||||||
### Save-file import
|
### Save-file import
|
||||||
|
|
||||||
**Settings → Import from a game save.** Reads the game's own seen/owned
|
**Settings → Import from a game save.** Reads the game's own seen/owned
|
||||||
bitfields — not an approximation — from:
|
bitfields — not an approximation — from:
|
||||||
|
|
||||||
| Gen | Games | Format |
|
| Gen | Games | Format |
|
||||||
| --- | --- | --- |
|
| --- | -------------------------------------------------- | -------------------------------------------------------- |
|
||||||
| 1 | Red / Blue / Yellow | 32 KB SRAM (`.sav` / `.srm`) |
|
| 1 | Red / Blue / Yellow | 32 KB SRAM (`.sav` / `.srm`) |
|
||||||
| 2 | Gold / Silver / Crystal | 32 KB SRAM — checksum picks G/S vs Crystal |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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
|
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
|
validated structurally (nothing set past the last species; every caught
|
||||||
@ -150,6 +157,7 @@ Full local state (settings, per-game tracking, team, form tracking, shiny
|
|||||||
hunts, played games) also exports / imports as a single JSON file.
|
hunts, played games) also exports / imports as a single JSON file.
|
||||||
|
|
||||||
### Settings
|
### Settings
|
||||||
|
|
||||||
Theme (System / Light / Dark / Black OLED / Sepia) + accent colour; text
|
Theme (System / Light / Dark / Black OLED / Sepia) + accent colour; text
|
||||||
size; an in-app reduce-motion override (the OS preference is always
|
size; an in-app reduce-motion override (the OS preference is always
|
||||||
respected too); sprite style (modern pixel / game-era pixel / official
|
respected too); sprite style (modern pixel / game-era pixel / official
|
||||||
@ -157,6 +165,7 @@ artwork / HOME); haptic feedback on catch (Vibration API); which screen to
|
|||||||
land on at launch; JSON export / import; cache and tracking resets.
|
land on at launch; JSON export / import; cache and tracking resets.
|
||||||
|
|
||||||
### PWA
|
### PWA
|
||||||
|
|
||||||
Workbox service worker: precache the app shell + snapshot; stale-while-
|
Workbox service worker: precache the app shell + snapshot; stale-while-
|
||||||
revalidate for PokéAPI JSON (30-day TTL); cache-first LRU for sprites;
|
revalidate for PokéAPI JSON (30-day TTL); cache-first LRU for sprites;
|
||||||
in-app update toast. Installable with maskable icons, `beforeinstallprompt`
|
in-app update toast. Installable with maskable icons, `beforeinstallprompt`
|
||||||
@ -169,12 +178,12 @@ Type chart.
|
|||||||
|
|
||||||
### Data & storage
|
### Data & storage
|
||||||
|
|
||||||
| Layer | Where | Notes |
|
| Layer | Where | Notes |
|
||||||
| --- | --- | --- |
|
| ---------------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| Preferences + tracking | `localStorage` (`pdx.*` keys) | Tiny, synchronous, restored on reload. |
|
| 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. |
|
| 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). |
|
| 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. |
|
| Sprites | PokéAPI sprites repo, cache-first | Capped LRU. |
|
||||||
|
|
||||||
The list, search, sort, filters and game switching run **entirely off the
|
The list, search, sort, filters and game switching run **entirely off the
|
||||||
snapshot** — no network until you open a detail page.
|
snapshot** — no network until you open a detail page.
|
||||||
|
|||||||
@ -10,8 +10,8 @@ branches.
|
|||||||
|
|
||||||
Please **don't** open a public issue for a security problem.
|
Please **don't** open a public issue for a security problem.
|
||||||
|
|
||||||
Use GitHub's **private vulnerability reporting** (the *Report a
|
Use GitHub's **private vulnerability reporting** (the _Report a
|
||||||
vulnerability* button under the repository's *Security* tab). Include what
|
vulnerability_ button under the repository's _Security_ tab). Include what
|
||||||
you found, how to reproduce it, and the impact you think it has. You'll get
|
you found, how to reproduce it, and the impact you think it has. You'll get
|
||||||
an acknowledgement as soon as possible.
|
an acknowledgement as soon as possible.
|
||||||
|
|
||||||
|
|||||||
@ -6,5 +6,5 @@ services:
|
|||||||
BASE_PATH: /
|
BASE_PATH: /
|
||||||
image: pocketdex
|
image: pocketdex
|
||||||
ports:
|
ports:
|
||||||
- "8080:80"
|
- '8080:80'
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@ -2,10 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
name="viewport"
|
|
||||||
content="width=device-width, initial-scale=1, viewport-fit=cover"
|
|
||||||
/>
|
|
||||||
<meta name="theme-color" content="#b3161a" />
|
<meta name="theme-color" content="#b3161a" />
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
|
|||||||
@ -111,9 +111,7 @@ async function isFresh() {
|
|||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
if (!FORCE && (await isFresh())) {
|
if (!FORCE && (await isFresh())) {
|
||||||
console.log(
|
console.log(`snapshot.json is fresh (< ${MAX_AGE_DAYS} days old). Use --force to rebuild.`);
|
||||||
`snapshot.json is fresh (< ${MAX_AGE_DAYS} days old). Use --force to rebuild.`,
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,15 +168,11 @@ async function main() {
|
|||||||
pokedexKeys: data.pokedexes.map((x) => x.name),
|
pokedexKeys: data.pokedexes.map((x) => x.name),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
versionGroups.sort(
|
versionGroups.sort((a, b) => a.generation - b.generation || a.key.localeCompare(b.key));
|
||||||
(a, b) => a.generation - b.generation || a.key.localeCompare(b.key),
|
|
||||||
);
|
|
||||||
|
|
||||||
// ---- Species: the data-rich part -----------------------------
|
// ---- Species: the data-rich part -----------------------------
|
||||||
const speciesIndex = await api('pokemon-species?limit=100000');
|
const speciesIndex = await api('pokemon-species?limit=100000');
|
||||||
const ids = speciesIndex.results
|
const ids = speciesIndex.results.map((r) => idFromUrl(r.url)).sort((a, b) => a - b);
|
||||||
.map((r) => idFromUrl(r.url))
|
|
||||||
.sort((a, b) => a - b);
|
|
||||||
|
|
||||||
const statMap = (pk) => {
|
const statMap = (pk) => {
|
||||||
const s = Object.fromEntries(pk.stats.map((x) => [x.stat.name, x.base_stat]));
|
const s = Object.fromEntries(pk.stats.map((x) => [x.stat.name, x.base_stat]));
|
||||||
@ -192,17 +186,21 @@ async function main() {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
const bstOf = (st) => st.hp + st.atk + st.def + st.spa + st.spd + st.spe;
|
const bstOf = (st) => st.hp + st.atk + st.def + st.spa + st.spd + st.spe;
|
||||||
const abilKey = (pk) => pk.abilities.map((a) => a.ability.name).sort().join(',');
|
const abilKey = (pk) =>
|
||||||
|
pk.abilities
|
||||||
|
.map((a) => a.ability.name)
|
||||||
|
.sort()
|
||||||
|
.join(',');
|
||||||
const statKey = (st) => `${st.hp}/${st.atk}/${st.def}/${st.spa}/${st.spd}/${st.spe}`;
|
const statKey = (st) => `${st.hp}/${st.atk}/${st.def}/${st.spa}/${st.spd}/${st.spe}`;
|
||||||
|
|
||||||
console.log(`Fetching ${ids.length} species (stats, typings, flags, forms) …`);
|
console.log(`Fetching ${ids.length} species (stats, typings, flags, forms) …`);
|
||||||
const species = await mapLimit(ids, CONCURRENCY, async (id) => {
|
const species = await mapLimit(ids, CONCURRENCY, async (id) => {
|
||||||
const [pk, sp] = await Promise.all([
|
const [pk, sp] = await Promise.all([api(`pokemon/${id}`), api(`pokemon-species/${id}`)]);
|
||||||
api(`pokemon/${id}`),
|
|
||||||
api(`pokemon-species/${id}`),
|
|
||||||
]);
|
|
||||||
const stats = statMap(pk);
|
const stats = statMap(pk);
|
||||||
const baseTypes = pk.types.slice().sort((a, b) => a.slot - b.slot).map((t) => t.type.name);
|
const baseTypes = pk.types
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => a.slot - b.slot)
|
||||||
|
.map((t) => t.type.name);
|
||||||
const baseAbil = abilKey(pk);
|
const baseAbil = abilKey(pk);
|
||||||
const baseStatKey = statKey(stats);
|
const baseStatKey = statKey(stats);
|
||||||
|
|
||||||
@ -226,7 +224,10 @@ async function main() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const fstats = statMap(fp);
|
const fstats = statMap(fp);
|
||||||
const ftypes = fp.types.slice().sort((a, b) => a.slot - b.slot).map((t) => t.type.name);
|
const ftypes = fp.types
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => a.slot - b.slot)
|
||||||
|
.map((t) => t.type.name);
|
||||||
const cat = formCategory(slug, sp.name);
|
const cat = formCategory(slug, sp.name);
|
||||||
const entry = {
|
const entry = {
|
||||||
slug,
|
slug,
|
||||||
@ -296,7 +297,10 @@ async function main() {
|
|||||||
.map((t) => t.type.name),
|
.map((t) => t.type.name),
|
||||||
pastTypes: (pk.past_types || []).map((p) => ({
|
pastTypes: (pk.past_types || []).map((p) => ({
|
||||||
gen: idFromUrl(p.generation.url),
|
gen: idFromUrl(p.generation.url),
|
||||||
types: p.types.slice().sort((a, b) => a.slot - b.slot).map((t) => t.type.name),
|
types: p.types
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => a.slot - b.slot)
|
||||||
|
.map((t) => t.type.name),
|
||||||
})),
|
})),
|
||||||
stats,
|
stats,
|
||||||
bst: stats.hp + stats.atk + stats.def + stats.spa + stats.spd + stats.spe,
|
bst: stats.hp + stats.atk + stats.def + stats.spa + stats.spd + stats.spe,
|
||||||
@ -326,7 +330,9 @@ async function main() {
|
|||||||
// exclusive there. Wild-only — gift/trade/evolution exclusives are missed.
|
// exclusive there. Wild-only — gift/trade/evolution exclusives are missed.
|
||||||
const versionToVg = {};
|
const versionToVg = {};
|
||||||
for (const vg of versionGroups) for (const v of vg.versions) versionToVg[v] = vg.key;
|
for (const vg of versionGroups) for (const v of vg.versions) versionToVg[v] = vg.key;
|
||||||
const vgVersionCount = Object.fromEntries(versionGroups.map((vg) => [vg.key, vg.versions.length]));
|
const vgVersionCount = Object.fromEntries(
|
||||||
|
versionGroups.map((vg) => [vg.key, vg.versions.length]),
|
||||||
|
);
|
||||||
console.log(`Fetching wild encounters for ${species.length} species …`);
|
console.log(`Fetching wild encounters for ${species.length} species …`);
|
||||||
await mapLimit(species, CONCURRENCY, async (sp) => {
|
await mapLimit(species, CONCURRENCY, async (sp) => {
|
||||||
let enc;
|
let enc;
|
||||||
@ -410,10 +416,7 @@ async function main() {
|
|||||||
name: a.name,
|
name: a.name,
|
||||||
generation: idFromUrl(a.generation.url),
|
generation: idFromUrl(a.generation.url),
|
||||||
isMainSeries: a.is_main_series,
|
isMainSeries: a.is_main_series,
|
||||||
effect:
|
effect: (en && (en.short_effect || en.effect)) || (flavour && flavour.flavor_text) || '',
|
||||||
(en && (en.short_effect || en.effect)) ||
|
|
||||||
(flavour && flavour.flavor_text) ||
|
|
||||||
'',
|
|
||||||
pokemon: [
|
pokemon: [
|
||||||
...new Set(a.pokemon.map((x) => idFromUrl(x.pokemon.url)).filter((n) => n <= 100000)),
|
...new Set(a.pokemon.map((x) => idFromUrl(x.pokemon.url)).filter((n) => n <= 100000)),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -13,7 +13,11 @@ import { prettify } from '../data/pokedex-resolver.js';
|
|||||||
* gradient, a type-colored spotlight behind an oversized sprite that lifts
|
* gradient, a type-colored spotlight behind an oversized sprite that lifts
|
||||||
* above the card, a number chip, and a big ghost number in the corner.
|
* above the card, a number chip, and a big ghost number in the corner.
|
||||||
*/
|
*/
|
||||||
export function Card(species, number, { spriteStyle = 'official', versionGroup, gen = 9, boxed = false, metric = null } = {}) {
|
export function Card(
|
||||||
|
species,
|
||||||
|
number,
|
||||||
|
{ spriteStyle = 'official', versionGroup, gen = 9, boxed = false, metric = null } = {},
|
||||||
|
) {
|
||||||
const state = entry(species.id);
|
const state = entry(species.id);
|
||||||
const types = typesForGen(species, gen);
|
const types = typesForGen(species, gen);
|
||||||
const mainType = types[0] || 'normal';
|
const mainType = types[0] || 'normal';
|
||||||
@ -61,7 +65,8 @@ export function Card(species, number, { spriteStyle = 'official', versionGroup,
|
|||||||
onclick: () => {
|
onclick: () => {
|
||||||
// Tag this sprite so the router's view transition morphs it into
|
// Tag this sprite so the router's view transition morphs it into
|
||||||
// the detail hero.
|
// the detail hero.
|
||||||
for (const n of document.querySelectorAll('.card__art.is-morph')) n.classList.remove('is-morph');
|
for (const n of document.querySelectorAll('.card__art.is-morph'))
|
||||||
|
n.classList.remove('is-morph');
|
||||||
art.classList.add('is-morph');
|
art.classList.add('is-morph');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -85,7 +90,11 @@ export function Card(species, number, { spriteStyle = 'official', versionGroup,
|
|||||||
(() => {
|
(() => {
|
||||||
const ex = versionGroup && versionGroup !== 'all' && species.exclusiveIn?.[versionGroup];
|
const ex = versionGroup && versionGroup !== 'all' && species.exclusiveIn?.[versionGroup];
|
||||||
return ex
|
return ex
|
||||||
? el('span', { class: 'card__excl', title: 'Version exclusive (wild)' }, `${ex.map(prettify).join(' & ')} only`)
|
? el(
|
||||||
|
'span',
|
||||||
|
{ class: 'card__excl', title: 'Version exclusive (wild)' },
|
||||||
|
`${ex.map(prettify).join(' & ')} only`,
|
||||||
|
)
|
||||||
: null;
|
: null;
|
||||||
})(),
|
})(),
|
||||||
metric ? el('span', { class: 'card__metric' }, metric) : null,
|
metric ? el('span', { class: 'card__metric' }, metric) : null,
|
||||||
|
|||||||
@ -34,7 +34,10 @@ function nodeEl(stage, style, versionGroup, depth) {
|
|||||||
* surviving ancestor — so Pikachu → Raichu still shows in Red/Blue even
|
* surviving ancestor — so Pikachu → Raichu still shows in Red/Blue even
|
||||||
* though Pichu doesn't, and Bellossom never appears there.
|
* though Pichu doesn't, and Bellossom never appears there.
|
||||||
*/
|
*/
|
||||||
export function EvolutionChain(chain, { style = 'default', versionGroup, genOf = () => 1, maxGen = 9 } = {}) {
|
export function EvolutionChain(
|
||||||
|
chain,
|
||||||
|
{ style = 'default', versionGroup, genOf = () => 1, maxGen = 9 } = {},
|
||||||
|
) {
|
||||||
const nodes = new Map();
|
const nodes = new Map();
|
||||||
let pruned = false;
|
let pruned = false;
|
||||||
|
|
||||||
@ -95,9 +98,7 @@ export function EvolutionChain(chain, { style = 'default', versionGroup, genOf =
|
|||||||
roots.forEach((r) => renderNode(r, 0));
|
roots.forEach((r) => renderNode(r, 0));
|
||||||
|
|
||||||
if (pruned) {
|
if (pruned) {
|
||||||
wrap.append(
|
wrap.append(el('p', { class: 'evo__note' }, 'Cross-generation stages hidden for this game.'));
|
||||||
el('p', { class: 'evo__note' }, 'Cross-generation stages hidden for this game.'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return wrap;
|
return wrap;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -96,9 +96,7 @@ export async function openGameSheet() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
for (const { generation, versionGroups } of versionGroupsByGeneration(snap)) {
|
for (const { generation, versionGroups } of versionGroupsByGeneration(snap)) {
|
||||||
list.append(
|
list.append(el('h3', { class: 'gsheet__gen' }, `Gen ${generation.id} · ${generation.name}`));
|
||||||
el('h3', { class: 'gsheet__gen' }, `Gen ${generation.id} · ${generation.name}`),
|
|
||||||
);
|
|
||||||
const grid = el('div', { class: 'gsheet__grid' });
|
const grid = el('div', { class: 'gsheet__grid' });
|
||||||
for (const vg of versionGroups) {
|
for (const vg of versionGroups) {
|
||||||
const active = vg.key === st.versionGroup;
|
const active = vg.key === st.versionGroup;
|
||||||
@ -126,7 +124,11 @@ export async function openGameSheet() {
|
|||||||
'span',
|
'span',
|
||||||
{ class: 'gamecard__foot' },
|
{ class: 'gamecard__foot' },
|
||||||
el('span', { class: 'gamecard__meta' }, vg.versions.map(prettify).join(' / ')),
|
el('span', { class: 'gamecard__meta' }, vg.versions.map(prettify).join(' / ')),
|
||||||
el('span', { class: 'gamecard__dex' }, `${dexCount} ${dexCount === 1 ? 'dex' : 'dexes'}`),
|
el(
|
||||||
|
'span',
|
||||||
|
{ class: 'gamecard__dex' },
|
||||||
|
`${dexCount} ${dexCount === 1 ? 'dex' : 'dexes'}`,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -6,8 +6,7 @@ import { TypeChip } from './TypeChip.js';
|
|||||||
export async function machineNumber(moveData, versionGroupKey) {
|
export async function machineNumber(moveData, versionGroupKey) {
|
||||||
const list = moveData.machines || [];
|
const list = moveData.machines || [];
|
||||||
if (!list.length) return null;
|
if (!list.length) return null;
|
||||||
const hit =
|
const hit = list.find((x) => x.version_group.name === versionGroupKey) || list[list.length - 1];
|
||||||
list.find((x) => x.version_group.name === versionGroupKey) || list[list.length - 1];
|
|
||||||
try {
|
try {
|
||||||
const mc = await getMachine(Number(hit.machine.url.replace(/\/$/, '').split('/').pop()));
|
const mc = await getMachine(Number(hit.machine.url.replace(/\/$/, '').split('/').pop()));
|
||||||
const m = mc.item.name.match(/^([a-z]+)0*(\d+)$/i);
|
const m = mc.item.name.match(/^([a-z]+)0*(\d+)$/i);
|
||||||
@ -53,7 +52,15 @@ function englishText(entries, key) {
|
|||||||
// Before Gen 4, a move's physical/special class was decided by its TYPE,
|
// Before Gen 4, a move's physical/special class was decided by its TYPE,
|
||||||
// not set per move.
|
// not set per move.
|
||||||
const PHYSICAL_TYPES = new Set([
|
const PHYSICAL_TYPES = new Set([
|
||||||
'normal', 'fighting', 'poison', 'ground', 'flying', 'bug', 'rock', 'ghost', 'steel',
|
'normal',
|
||||||
|
'fighting',
|
||||||
|
'poison',
|
||||||
|
'ground',
|
||||||
|
'flying',
|
||||||
|
'bug',
|
||||||
|
'rock',
|
||||||
|
'ghost',
|
||||||
|
'steel',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export function forGeneration(d, gen, genOfVg) {
|
export function forGeneration(d, gen, genOfVg) {
|
||||||
@ -145,7 +152,11 @@ export function MovesList(pokemonMoves, { versionGroupKey, gen = 9, genOfVg = ()
|
|||||||
const enrichers = [];
|
const enrichers = [];
|
||||||
|
|
||||||
for (const m of moves) {
|
for (const m of moves) {
|
||||||
const meta = el('span', { class: 'moverow__meta' }, el('span', { class: 'moverow__pending' }, '…'));
|
const meta = el(
|
||||||
|
'span',
|
||||||
|
{ class: 'moverow__meta' },
|
||||||
|
el('span', { class: 'moverow__pending' }, '…'),
|
||||||
|
);
|
||||||
const body = el('div', { class: 'movebody', hidden: true });
|
const body = el('div', { class: 'movebody', hidden: true });
|
||||||
const tmCell = method === 'machine' ? el('span', { class: 'moverow__tm' }) : null;
|
const tmCell = method === 'machine' ? el('span', { class: 'moverow__tm' }) : null;
|
||||||
let bodyFilled = false;
|
let bodyFilled = false;
|
||||||
@ -179,7 +190,9 @@ export function MovesList(pokemonMoves, { versionGroupKey, gen = 9, genOfVg = ()
|
|||||||
fillBody(body, m.data, v);
|
fillBody(body, m.data, v);
|
||||||
await fillTm();
|
await fillTm();
|
||||||
} catch {
|
} catch {
|
||||||
body.replaceChildren(el('span', { class: 'moverow__pending' }, 'Details unavailable.'));
|
body.replaceChildren(
|
||||||
|
el('span', { class: 'moverow__pending' }, 'Details unavailable.'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -268,9 +281,12 @@ function fillBody(body, d, v) {
|
|||||||
if (d.priority) bits.push(`Priority ${d.priority > 0 ? '+' : ''}${d.priority}`);
|
if (d.priority) bits.push(`Priority ${d.priority > 0 ? '+' : ''}${d.priority}`);
|
||||||
if (d.target?.name) bits.push(`Target: ${d.target.name.replace(/-/g, ' ')}`);
|
if (d.target?.name) bits.push(`Target: ${d.target.name.replace(/-/g, ' ')}`);
|
||||||
if (d.meta?.ailment?.name && d.meta.ailment.name !== 'none') {
|
if (d.meta?.ailment?.name && d.meta.ailment.name !== 'none') {
|
||||||
bits.push(`May ${d.meta.ailment.name.replace(/-/g, ' ')}${d.meta.ailment_chance ? ` (${d.meta.ailment_chance}%)` : ''}`);
|
bits.push(
|
||||||
|
`May ${d.meta.ailment.name.replace(/-/g, ' ')}${d.meta.ailment_chance ? ` (${d.meta.ailment_chance}%)` : ''}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (d.meta?.drain) bits.push(`${d.meta.drain > 0 ? 'Drains' : 'Recoil'} ${Math.abs(d.meta.drain)}%`);
|
if (d.meta?.drain)
|
||||||
|
bits.push(`${d.meta.drain > 0 ? 'Drains' : 'Recoil'} ${Math.abs(d.meta.drain)}%`);
|
||||||
if (d.meta?.healing) bits.push(`Heals ${d.meta.healing}%`);
|
if (d.meta?.healing) bits.push(`Heals ${d.meta.healing}%`);
|
||||||
if (d.meta?.crit_rate) bits.push(`+${d.meta.crit_rate} crit rate`);
|
if (d.meta?.crit_rate) bits.push(`+${d.meta.crit_rate} crit rate`);
|
||||||
if (d.meta?.flinch_chance) bits.push(`Flinch ${d.meta.flinch_chance}%`);
|
if (d.meta?.flinch_chance) bits.push(`Flinch ${d.meta.flinch_chance}%`);
|
||||||
|
|||||||
@ -4,7 +4,13 @@ import { settings } from '../store/settings.js';
|
|||||||
|
|
||||||
const ITEMS = [
|
const ITEMS = [
|
||||||
{ label: 'Dex', icon: '▦', href: '#/', match: (h) => h === '#/' || h === '' || h === '#' },
|
{ label: 'Dex', icon: '▦', href: '#/', match: (h) => h === '#/' || h === '' || h === '#' },
|
||||||
{ label: 'Team', icon: '⛨', href: '#/team', match: (h) => h.startsWith('#/team'), optional: 'showTeamNav' },
|
{
|
||||||
|
label: 'Team',
|
||||||
|
icon: '⛨',
|
||||||
|
href: '#/team',
|
||||||
|
match: (h) => h.startsWith('#/team'),
|
||||||
|
optional: 'showTeamNav',
|
||||||
|
},
|
||||||
{ label: 'Games', icon: '◉', action: openGameSheet },
|
{ label: 'Games', icon: '◉', action: openGameSheet },
|
||||||
{ label: 'Search', icon: '⌕', href: '#/search', match: (h) => h.startsWith('#/search') },
|
{ label: 'Search', icon: '⌕', href: '#/search', match: (h) => h.startsWith('#/search') },
|
||||||
{ label: 'Settings', icon: '⚙', href: '#/settings', match: (h) => h.startsWith('#/settings') },
|
{ label: 'Settings', icon: '⚙', href: '#/settings', match: (h) => h.startsWith('#/settings') },
|
||||||
@ -28,7 +34,11 @@ export function Nav() {
|
|||||||
el('span', { class: 'nav__label' }, item.label),
|
el('span', { class: 'nav__label' }, item.label),
|
||||||
];
|
];
|
||||||
return item.action
|
return item.action
|
||||||
? el('button', { class: 'nav__link', type: 'button', onclick: () => item.action() }, ...inner)
|
? el(
|
||||||
|
'button',
|
||||||
|
{ class: 'nav__link', type: 'button', onclick: () => item.action() },
|
||||||
|
...inner,
|
||||||
|
)
|
||||||
: el('a', { class: 'nav__link', href: item.href }, ...inner);
|
: el('a', { class: 'nav__link', href: item.href }, ...inner);
|
||||||
});
|
});
|
||||||
nav.replaceChildren(el('span', { class: 'nav__brand' }, 'Pocketdex'), ...links);
|
nav.replaceChildren(el('span', { class: 'nav__brand' }, 'Pocketdex'), ...links);
|
||||||
|
|||||||
@ -88,7 +88,11 @@ export async function openPokemonPicker(onPick, { closeAfterPick = false } = {})
|
|||||||
'div',
|
'div',
|
||||||
{ class: 'gsheet__head' },
|
{ class: 'gsheet__head' },
|
||||||
el('h2', {}, 'Add a Pokémon'),
|
el('h2', {}, 'Add a Pokémon'),
|
||||||
el('button', { class: 'gsheet__close', type: 'button', 'aria-label': 'Close', onclick: close }, '✕'),
|
el(
|
||||||
|
'button',
|
||||||
|
{ class: 'gsheet__close', type: 'button', 'aria-label': 'Close', onclick: close },
|
||||||
|
'✕',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
dragToClose(panel, head, close);
|
dragToClose(panel, head, close);
|
||||||
panel.append(head, input, results);
|
panel.append(head, input, results);
|
||||||
|
|||||||
@ -40,7 +40,12 @@ export function ProgressRing() {
|
|||||||
const value = el('span', { class: 'ring__value' }, num, tail);
|
const value = el('span', { class: 'ring__value' }, num, tail);
|
||||||
const node = el(
|
const node = el(
|
||||||
'a',
|
'a',
|
||||||
{ class: 'ring', href: '#/progress', title: 'Progress by game', 'aria-label': 'Progress by game' },
|
{
|
||||||
|
class: 'ring',
|
||||||
|
href: '#/progress',
|
||||||
|
title: 'Progress by game',
|
||||||
|
'aria-label': 'Progress by game',
|
||||||
|
},
|
||||||
svg,
|
svg,
|
||||||
el('div', { class: 'ring__label' }, value),
|
el('div', { class: 'ring__label' }, value),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import { el } from '../lib/dom.js';
|
import { el } from '../lib/dom.js';
|
||||||
import { gameSpriteUrl } from '../data/game-sprites.js';
|
import { gameSpriteUrl } from '../data/game-sprites.js';
|
||||||
|
|
||||||
const SPRITES =
|
const SPRITES = 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon';
|
||||||
'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon';
|
|
||||||
|
|
||||||
function pixel(id, shiny) {
|
function pixel(id, shiny) {
|
||||||
return `${SPRITES}${shiny ? '/shiny' : ''}/${id}.png`;
|
return `${SPRITES}${shiny ? '/shiny' : ''}/${id}.png`;
|
||||||
|
|||||||
@ -2,8 +2,7 @@ import { el } from '../lib/dom.js';
|
|||||||
import { Sprite } from './Sprite.js';
|
import { Sprite } from './Sprite.js';
|
||||||
import { settings } from '../store/settings.js';
|
import { settings } from '../store/settings.js';
|
||||||
|
|
||||||
const bar = (w, h = 14) =>
|
const bar = (w, h = 14) => el('span', { class: 'sk-bar', style: `width:${w};height:${h}px` });
|
||||||
el('span', { class: 'sk-bar', style: `width:${w};height:${h}px` });
|
|
||||||
|
|
||||||
const facts = () =>
|
const facts = () =>
|
||||||
el(
|
el(
|
||||||
@ -43,11 +42,7 @@ export function detailSkeleton(match) {
|
|||||||
el(
|
el(
|
||||||
'div',
|
'div',
|
||||||
{ class: 'psheet' },
|
{ class: 'psheet' },
|
||||||
el(
|
el('div', { class: 'psheet__tabs' }, ...Array.from({ length: 5 }, () => bar('58px', 18))),
|
||||||
'div',
|
|
||||||
{ class: 'psheet__tabs' },
|
|
||||||
...Array.from({ length: 5 }, () => bar('58px', 18)),
|
|
||||||
),
|
|
||||||
el('div', { class: 'ppanel' }, bar('92%'), bar('84%'), bar('66%'), facts()),
|
el('div', { class: 'ppanel' }, bar('92%'), bar('84%'), bar('66%'), facts()),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,8 +4,7 @@
|
|||||||
* group) to the closest sprite folder. Games newer than Gen 7 shipped no
|
* group) to the closest sprite folder. Games newer than Gen 7 shipped no
|
||||||
* pixel sprites — those return null and the caller falls back to HOME art.
|
* pixel sprites — those return null and the caller falls back to HOME art.
|
||||||
*/
|
*/
|
||||||
const REPO =
|
const REPO = 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions';
|
||||||
'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions';
|
|
||||||
|
|
||||||
const PATH_BY_VERSION_GROUP = {
|
const PATH_BY_VERSION_GROUP = {
|
||||||
'red-blue': 'generation-i/red-blue',
|
'red-blue': 'generation-i/red-blue',
|
||||||
@ -26,8 +25,7 @@ const PATH_BY_VERSION_GROUP = {
|
|||||||
'omega-ruby-alpha-sapphire': 'generation-vi/omegaruby-alphasapphire',
|
'omega-ruby-alpha-sapphire': 'generation-vi/omegaruby-alphasapphire',
|
||||||
'sun-moon': 'generation-vii/ultra-sun-ultra-moon',
|
'sun-moon': 'generation-vii/ultra-sun-ultra-moon',
|
||||||
'ultra-sun-ultra-moon': 'generation-vii/ultra-sun-ultra-moon',
|
'ultra-sun-ultra-moon': 'generation-vii/ultra-sun-ultra-moon',
|
||||||
'brilliant-diamond-and-shining-pearl':
|
'brilliant-diamond-and-shining-pearl': 'generation-viii/brilliant-diamond-shining-pearl',
|
||||||
'generation-viii/brilliant-diamond-shining-pearl',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** URL for a game-era pixel sprite, or null if that game has none. */
|
/** URL for a game-era pixel sprite, or null if that game has none. */
|
||||||
|
|||||||
@ -4,18 +4,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export function prettify(key) {
|
export function prettify(key) {
|
||||||
return key
|
return key.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
||||||
.replace(/-/g, ' ')
|
|
||||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Regional dexes belonging to a version group, in the API's order. */
|
/** Regional dexes belonging to a version group, in the API's order. */
|
||||||
export function dexesForVersionGroup(snap, versionGroupKey) {
|
export function dexesForVersionGroup(snap, versionGroupKey) {
|
||||||
const vg = snap.versionGroupByKey.get(versionGroupKey);
|
const vg = snap.versionGroupByKey.get(versionGroupKey);
|
||||||
const keys = vg && vg.pokedexKeys.length ? vg.pokedexKeys : ['national'];
|
const keys = vg && vg.pokedexKeys.length ? vg.pokedexKeys : ['national'];
|
||||||
return keys
|
return keys.map((k) => snap.pokedexByKey.get(k)).filter(Boolean);
|
||||||
.map((k) => snap.pokedexByKey.get(k))
|
|
||||||
.filter(Boolean);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -5,9 +5,24 @@
|
|||||||
* CHART[attacking][defending] holds only the non-1x multipliers.
|
* CHART[attacking][defending] holds only the non-1x multipliers.
|
||||||
*/
|
*/
|
||||||
export const TYPES = [
|
export const TYPES = [
|
||||||
'normal', 'fire', 'water', 'electric', 'grass', 'ice', 'fighting', 'poison',
|
'normal',
|
||||||
'ground', 'flying', 'psychic', 'bug', 'rock', 'ghost', 'dragon', 'dark',
|
'fire',
|
||||||
'steel', 'fairy',
|
'water',
|
||||||
|
'electric',
|
||||||
|
'grass',
|
||||||
|
'ice',
|
||||||
|
'fighting',
|
||||||
|
'poison',
|
||||||
|
'ground',
|
||||||
|
'flying',
|
||||||
|
'psychic',
|
||||||
|
'bug',
|
||||||
|
'rock',
|
||||||
|
'ghost',
|
||||||
|
'dragon',
|
||||||
|
'dark',
|
||||||
|
'steel',
|
||||||
|
'fairy',
|
||||||
];
|
];
|
||||||
|
|
||||||
const CHART = {
|
const CHART = {
|
||||||
@ -15,14 +30,48 @@ const CHART = {
|
|||||||
fire: { fire: 0.5, water: 0.5, grass: 2, ice: 2, bug: 2, rock: 0.5, dragon: 0.5, steel: 2 },
|
fire: { fire: 0.5, water: 0.5, grass: 2, ice: 2, bug: 2, rock: 0.5, dragon: 0.5, steel: 2 },
|
||||||
water: { fire: 2, water: 0.5, grass: 0.5, ground: 2, rock: 2, dragon: 0.5 },
|
water: { fire: 2, water: 0.5, grass: 0.5, ground: 2, rock: 2, dragon: 0.5 },
|
||||||
electric: { water: 2, electric: 0.5, grass: 0.5, ground: 0, flying: 2, dragon: 0.5 },
|
electric: { water: 2, electric: 0.5, grass: 0.5, ground: 0, flying: 2, dragon: 0.5 },
|
||||||
grass: { fire: 0.5, water: 2, grass: 0.5, poison: 0.5, ground: 2, flying: 0.5, bug: 0.5, rock: 2, dragon: 0.5, steel: 0.5 },
|
grass: {
|
||||||
|
fire: 0.5,
|
||||||
|
water: 2,
|
||||||
|
grass: 0.5,
|
||||||
|
poison: 0.5,
|
||||||
|
ground: 2,
|
||||||
|
flying: 0.5,
|
||||||
|
bug: 0.5,
|
||||||
|
rock: 2,
|
||||||
|
dragon: 0.5,
|
||||||
|
steel: 0.5,
|
||||||
|
},
|
||||||
ice: { fire: 0.5, water: 0.5, grass: 2, ice: 0.5, ground: 2, flying: 2, dragon: 2, steel: 0.5 },
|
ice: { fire: 0.5, water: 0.5, grass: 2, ice: 0.5, ground: 2, flying: 2, dragon: 2, steel: 0.5 },
|
||||||
fighting: { normal: 2, ice: 2, poison: 0.5, flying: 0.5, psychic: 0.5, bug: 0.5, rock: 2, ghost: 0, dark: 2, steel: 2, fairy: 0.5 },
|
fighting: {
|
||||||
|
normal: 2,
|
||||||
|
ice: 2,
|
||||||
|
poison: 0.5,
|
||||||
|
flying: 0.5,
|
||||||
|
psychic: 0.5,
|
||||||
|
bug: 0.5,
|
||||||
|
rock: 2,
|
||||||
|
ghost: 0,
|
||||||
|
dark: 2,
|
||||||
|
steel: 2,
|
||||||
|
fairy: 0.5,
|
||||||
|
},
|
||||||
poison: { grass: 2, poison: 0.5, ground: 0.5, rock: 0.5, ghost: 0.5, steel: 0, fairy: 2 },
|
poison: { grass: 2, poison: 0.5, ground: 0.5, rock: 0.5, ghost: 0.5, steel: 0, fairy: 2 },
|
||||||
ground: { fire: 2, electric: 2, grass: 0.5, poison: 2, flying: 0, bug: 0.5, rock: 2, steel: 2 },
|
ground: { fire: 2, electric: 2, grass: 0.5, poison: 2, flying: 0, bug: 0.5, rock: 2, steel: 2 },
|
||||||
flying: { electric: 0.5, grass: 2, fighting: 2, bug: 2, rock: 0.5, steel: 0.5 },
|
flying: { electric: 0.5, grass: 2, fighting: 2, bug: 2, rock: 0.5, steel: 0.5 },
|
||||||
psychic: { fighting: 2, poison: 2, psychic: 0.5, dark: 0, steel: 0.5 },
|
psychic: { fighting: 2, poison: 2, psychic: 0.5, dark: 0, steel: 0.5 },
|
||||||
bug: { fire: 0.5, grass: 2, fighting: 0.5, poison: 0.5, flying: 0.5, psychic: 2, ghost: 0.5, dark: 2, steel: 0.5, fairy: 0.5 },
|
bug: {
|
||||||
|
fire: 0.5,
|
||||||
|
grass: 2,
|
||||||
|
fighting: 0.5,
|
||||||
|
poison: 0.5,
|
||||||
|
flying: 0.5,
|
||||||
|
psychic: 2,
|
||||||
|
ghost: 0.5,
|
||||||
|
dark: 2,
|
||||||
|
steel: 0.5,
|
||||||
|
fairy: 0.5,
|
||||||
|
},
|
||||||
rock: { fire: 2, ice: 2, fighting: 0.5, ground: 0.5, flying: 2, bug: 2, steel: 0.5 },
|
rock: { fire: 2, ice: 2, fighting: 0.5, ground: 0.5, flying: 2, bug: 2, steel: 0.5 },
|
||||||
ghost: { normal: 0, psychic: 2, ghost: 2, dark: 0.5 },
|
ghost: { normal: 0, psychic: 2, ghost: 2, dark: 0.5 },
|
||||||
dragon: { dragon: 2, steel: 0.5, fairy: 0 },
|
dragon: { dragon: 2, steel: 0.5, fairy: 0 },
|
||||||
@ -94,7 +143,7 @@ export function offensiveSummary(atkTypes, gen = 9) {
|
|||||||
* Returns { '4': [...types], '2': [...], '0.5': [...], '0.25': [...], '0': [...] }
|
* Returns { '4': [...types], '2': [...], '0.5': [...], '0.25': [...], '0': [...] }
|
||||||
*/
|
*/
|
||||||
export function defensiveMatchups(defTypes, gen = 9) {
|
export function defensiveMatchups(defTypes, gen = 9) {
|
||||||
const buckets = { '4': [], '2': [], '0.5': [], '0.25': [], '0': [] };
|
const buckets = { 4: [], 2: [], 0.5: [], 0.25: [], 0: [] };
|
||||||
// Types that didn't exist yet can't be attacking: Steel/Dark before Gen 2,
|
// Types that didn't exist yet can't be attacking: Steel/Dark before Gen 2,
|
||||||
// Fairy before Gen 6.
|
// Fairy before Gen 6.
|
||||||
const absent = new Set(gen < 2 ? ['steel', 'dark', 'fairy'] : gen < 6 ? ['fairy'] : []);
|
const absent = new Set(gen < 2 ? ['steel', 'dark', 'fairy'] : gen < 6 ? ['fairy'] : []);
|
||||||
|
|||||||
@ -6,7 +6,11 @@ const easeOutCubic = (t) => 1 - Math.pow(1 - t, 3);
|
|||||||
* Tween a number into `node`'s text. Snaps straight to the value when the
|
* Tween a number into `node`'s text. Snaps straight to the value when the
|
||||||
* user prefers reduced motion. Returns a cancel fn.
|
* user prefers reduced motion. Returns a cancel fn.
|
||||||
*/
|
*/
|
||||||
export function countUp(node, to, { from = 0, duration = 550, decimals = 0, prefix = '', suffix = '', ease = easeOutCubic } = {}) {
|
export function countUp(
|
||||||
|
node,
|
||||||
|
to,
|
||||||
|
{ from = 0, duration = 550, decimals = 0, prefix = '', suffix = '', ease = easeOutCubic } = {},
|
||||||
|
) {
|
||||||
const fmt = (v) => `${prefix}${decimals ? v.toFixed(decimals) : Math.round(v)}${suffix}`;
|
const fmt = (v) => `${prefix}${decimals ? v.toFixed(decimals) : Math.round(v)}${suffix}`;
|
||||||
if (prefersReducedMotion() || duration <= 0 || from === to) {
|
if (prefersReducedMotion() || duration <= 0 || from === to) {
|
||||||
node.textContent = fmt(to);
|
node.textContent = fmt(to);
|
||||||
|
|||||||
@ -32,7 +32,17 @@ export function statAt(base, level, { iv = 31, ev = 0, nature = 'Hardy', statKey
|
|||||||
*
|
*
|
||||||
* Returns null for status moves or 0-power moves.
|
* Returns null for status moves or 0-power moves.
|
||||||
*/
|
*/
|
||||||
export function calcDamage({ level, power, moveType, atkTypes, defTypes, atkStat, defStat, gen, crit = false }) {
|
export function calcDamage({
|
||||||
|
level,
|
||||||
|
power,
|
||||||
|
moveType,
|
||||||
|
atkTypes,
|
||||||
|
defTypes,
|
||||||
|
atkStat,
|
||||||
|
defStat,
|
||||||
|
gen,
|
||||||
|
crit = false,
|
||||||
|
}) {
|
||||||
if (!power) return null;
|
if (!power) return null;
|
||||||
const base = Math.floor((Math.floor((2 * level) / 5 + 2) * power * (atkStat / defStat)) / 50) + 2;
|
const base = Math.floor((Math.floor((2 * level) / 5 + 2) * power * (atkStat / defStat)) / 50) + 2;
|
||||||
const stab = atkTypes.includes(moveType) ? 1.5 : 1;
|
const stab = atkTypes.includes(moveType) ? 1.5 : 1;
|
||||||
|
|||||||
@ -32,7 +32,16 @@ const GB_CHARS = (() => {
|
|||||||
t[0xa0 + i] = lo[i];
|
t[0xa0 + i] = lo[i];
|
||||||
}
|
}
|
||||||
for (let i = 0; i < 10; i++) t[0xf6 + i] = String(i);
|
for (let i = 0; i < 10; i++) t[0xf6 + i] = String(i);
|
||||||
Object.assign(t, { 0xe8: '.', 0xe3: '-', 0xf3: '/', 0xf4: ',', 0x9a: '(', 0x9b: ')', 0xe6: '?', 0xe7: '!' });
|
Object.assign(t, {
|
||||||
|
0xe8: '.',
|
||||||
|
0xe3: '-',
|
||||||
|
0xf3: '/',
|
||||||
|
0xf4: ',',
|
||||||
|
0x9a: '(',
|
||||||
|
0x9b: ')',
|
||||||
|
0xe6: '?',
|
||||||
|
0xe7: '!',
|
||||||
|
});
|
||||||
return t;
|
return t;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@ -47,7 +56,11 @@ const GBA_CHARS = (() => {
|
|||||||
t[0xd5 + i] = lo[i];
|
t[0xd5 + i] = lo[i];
|
||||||
}
|
}
|
||||||
Object.assign(t, {
|
Object.assign(t, {
|
||||||
0xad: '.', 0xae: '-', 0xba: '/', 0xac: ',', 0xb8: '&',
|
0xad: '.',
|
||||||
|
0xae: '-',
|
||||||
|
0xba: '/',
|
||||||
|
0xac: ',',
|
||||||
|
0xb8: '&',
|
||||||
});
|
});
|
||||||
return t;
|
return t;
|
||||||
})();
|
})();
|
||||||
@ -76,13 +89,21 @@ const sumBytes = (bytes, from, to) => {
|
|||||||
* Generation I — Red / Blue / Yellow (International, 32 KB SRAM)
|
* Generation I — Red / Blue / Yellow (International, 32 KB SRAM)
|
||||||
* ------------------------------------------------------------------ */
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
const G1 = { name: 0x2598, owned: 0x25a3, seen: 0x25b6, species: 151, ckByte: 0x3523, ckFrom: 0x2598, ckTo: 0x3522 };
|
const G1 = {
|
||||||
|
name: 0x2598,
|
||||||
|
owned: 0x25a3,
|
||||||
|
seen: 0x25b6,
|
||||||
|
species: 151,
|
||||||
|
ckByte: 0x3523,
|
||||||
|
ckFrom: 0x2598,
|
||||||
|
ckTo: 0x3522,
|
||||||
|
};
|
||||||
|
|
||||||
function tryGen1(bytes) {
|
function tryGen1(bytes) {
|
||||||
if (bytes.length < 0x8000) return null;
|
if (bytes.length < 0x8000) return null;
|
||||||
const caught = bitsToList(bytes, G1.owned, G1.species);
|
const caught = bitsToList(bytes, G1.owned, G1.species);
|
||||||
const seen = bitsToList(bytes, G1.seen, G1.species);
|
const seen = bitsToList(bytes, G1.seen, G1.species);
|
||||||
const checksumOk = ((~sumBytes(bytes, G1.ckFrom, G1.ckTo)) & 0xff) === bytes[G1.ckByte];
|
const checksumOk = (~sumBytes(bytes, G1.ckFrom, G1.ckTo) & 0xff) === bytes[G1.ckByte];
|
||||||
if (!seen.length && !caught.length) return null;
|
if (!seen.length && !caught.length) return null;
|
||||||
return {
|
return {
|
||||||
gen: 1,
|
gen: 1,
|
||||||
@ -101,8 +122,26 @@ function tryGen1(bytes) {
|
|||||||
* ------------------------------------------------------------------ */
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
const G2 = {
|
const G2 = {
|
||||||
gs: { name: 0x200b, owned: 0x2a4c, seen: 0x2a6c, ck: 0x2d69, ckFrom: 0x2009, ckTo: 0x2d68, label: 'Gold / Silver', vgs: ['gold-silver'] },
|
gs: {
|
||||||
cr: { name: 0x2009, owned: 0x2a27, seen: 0x2a47, ck: 0x2d0d, ckFrom: 0x2009, ckTo: 0x2b82, label: 'Crystal', vgs: ['crystal'] },
|
name: 0x200b,
|
||||||
|
owned: 0x2a4c,
|
||||||
|
seen: 0x2a6c,
|
||||||
|
ck: 0x2d69,
|
||||||
|
ckFrom: 0x2009,
|
||||||
|
ckTo: 0x2d68,
|
||||||
|
label: 'Gold / Silver',
|
||||||
|
vgs: ['gold-silver'],
|
||||||
|
},
|
||||||
|
cr: {
|
||||||
|
name: 0x2009,
|
||||||
|
owned: 0x2a27,
|
||||||
|
seen: 0x2a47,
|
||||||
|
ck: 0x2d0d,
|
||||||
|
ckFrom: 0x2009,
|
||||||
|
ckTo: 0x2b82,
|
||||||
|
label: 'Crystal',
|
||||||
|
vgs: ['crystal'],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const G2_SPECIES = 251;
|
const G2_SPECIES = 251;
|
||||||
|
|
||||||
@ -145,7 +184,9 @@ const SECTION_SIZE = 4096;
|
|||||||
const SECTIONS_PER_SLOT = 14;
|
const SECTIONS_PER_SLOT = 14;
|
||||||
const SLOT_SIZE = SECTION_SIZE * SECTIONS_PER_SLOT; // 57344
|
const SLOT_SIZE = SECTION_SIZE * SECTIONS_PER_SLOT; // 57344
|
||||||
const SIGNATURE = 0x08012025;
|
const SIGNATURE = 0x08012025;
|
||||||
const SECTION_DATA_SIZE = [3884, 3968, 3968, 3968, 3848, 3968, 3968, 3968, 3968, 3968, 3968, 3968, 3968, 2000];
|
const SECTION_DATA_SIZE = [
|
||||||
|
3884, 3968, 3968, 3968, 3848, 3968, 3968, 3968, 3968, 3968, 3968, 3968, 3968, 2000,
|
||||||
|
];
|
||||||
const DEX_OFFSET = 0x18;
|
const DEX_OFFSET = 0x18;
|
||||||
const DEX_MAGIC = DEX_OFFSET + 2; // 0xDA = RSE national, 0xB9 = FRLG national
|
const DEX_MAGIC = DEX_OFFSET + 2; // 0xDA = RSE national, 0xB9 = FRLG national
|
||||||
const G3_OWNED = DEX_OFFSET + 16;
|
const G3_OWNED = DEX_OFFSET + 16;
|
||||||
@ -179,7 +220,10 @@ function tryGen3(bytes) {
|
|||||||
if (bytes.length < SLOT_SIZE + SECTION_SIZE) return null;
|
if (bytes.length < SLOT_SIZE + SECTION_SIZE) return null;
|
||||||
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||||
const a = readSlot(view, 0);
|
const a = readSlot(view, 0);
|
||||||
const b = bytes.length >= 2 * SLOT_SIZE ? readSlot(view, SLOT_SIZE) : { sections: {}, counter: -1, valid: 0 };
|
const b =
|
||||||
|
bytes.length >= 2 * SLOT_SIZE
|
||||||
|
? readSlot(view, SLOT_SIZE)
|
||||||
|
: { sections: {}, counter: -1, valid: 0 };
|
||||||
if (a.valid < 3 && b.valid < 3) return null;
|
if (a.valid < 3 && b.valid < 3) return null;
|
||||||
const slot = b.counter > a.counter ? b : a;
|
const slot = b.counter > a.counter ? b : a;
|
||||||
const s0 = slot.sections[0];
|
const s0 = slot.sections[0];
|
||||||
@ -222,8 +266,15 @@ function ndsNormalise(bytes) {
|
|||||||
let hasMarker = false;
|
let hasMarker = false;
|
||||||
for (let i = 0; i <= tail.length - marker.length; i++) {
|
for (let i = 0; i <= tail.length - marker.length; i++) {
|
||||||
let ok = true;
|
let ok = true;
|
||||||
for (let j = 0; j < marker.length; j++) if (tail[i + j] !== marker.charCodeAt(j)) { ok = false; break; }
|
for (let j = 0; j < marker.length; j++)
|
||||||
if (ok) { hasMarker = true; break; }
|
if (tail[i + j] !== marker.charCodeAt(j)) {
|
||||||
|
ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (ok) {
|
||||||
|
hasMarker = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (hasMarker || bytes.length >= 0x80000) return bytes.subarray(0, 0x80000);
|
if (hasMarker || bytes.length >= 0x80000) return bytes.subarray(0, 0x80000);
|
||||||
}
|
}
|
||||||
@ -298,7 +349,12 @@ function readGen5Slot(bytes, slotBase, v) {
|
|||||||
const caught = bitsToList(bytes, p + GEN5.caught, GEN5.species);
|
const caught = bitsToList(bytes, p + GEN5.caught, GEN5.species);
|
||||||
const seen = orSeen(bytes, p, GEN5.seen, GEN5.size, GEN5.species);
|
const seen = orSeen(bytes, p, GEN5.seen, GEN5.size, GEN5.species);
|
||||||
if (!seen.length || caught.some((n) => !seen.includes(n))) return null;
|
if (!seen.length || caught.some((n) => !seen.includes(n))) return null;
|
||||||
return { seen, caught, label: v.label, vg: v.dex === GEN5.bw.dex ? 'black-white' : 'black-2-white-2' };
|
return {
|
||||||
|
seen,
|
||||||
|
caught,
|
||||||
|
label: v.label,
|
||||||
|
vg: v.dex === GEN5.bw.dex ? 'black-white' : 'black-2-white-2',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function tryGen5(raw) {
|
function tryGen5(raw) {
|
||||||
|
|||||||
@ -20,7 +20,11 @@ export function onSwipe(target, { onLeft, onRight, threshold = 60 } = {}) {
|
|||||||
// the page? (moves list, tab strip, coverage grid…) `dir` < 0 means the
|
// the page? (moves list, tab strip, coverage grid…) `dir` < 0 means the
|
||||||
// finger is moving left — that scrolls such an element to the right.
|
// finger is moving left — that scrolls such an element to the right.
|
||||||
const scrollsInside = (node, dir) => {
|
const scrollsInside = (node, dir) => {
|
||||||
for (let n = node; n && n !== document.body && n !== target.parentElement; n = n.parentElement) {
|
for (
|
||||||
|
let n = node;
|
||||||
|
n && n !== document.body && n !== target.parentElement;
|
||||||
|
n = n.parentElement
|
||||||
|
) {
|
||||||
if (n.scrollWidth - n.clientWidth > 2) {
|
if (n.scrollWidth - n.clientWidth > 2) {
|
||||||
const ox = getComputedStyle(n).overflowX;
|
const ox = getComputedStyle(n).overflowX;
|
||||||
if (ox === 'auto' || ox === 'scroll') {
|
if (ox === 'auto' || ox === 'scroll') {
|
||||||
|
|||||||
@ -2,9 +2,24 @@
|
|||||||
// custom properties once and memoising. Type colors do not change between
|
// custom properties once and memoising. Type colors do not change between
|
||||||
// light and dark, so the cache is safe for the session.
|
// light and dark, so the cache is safe for the session.
|
||||||
const TYPES = [
|
const TYPES = [
|
||||||
'normal', 'fire', 'water', 'electric', 'grass', 'ice', 'fighting', 'poison',
|
'normal',
|
||||||
'ground', 'flying', 'psychic', 'bug', 'rock', 'ghost', 'dragon', 'dark',
|
'fire',
|
||||||
'steel', 'fairy',
|
'water',
|
||||||
|
'electric',
|
||||||
|
'grass',
|
||||||
|
'ice',
|
||||||
|
'fighting',
|
||||||
|
'poison',
|
||||||
|
'ground',
|
||||||
|
'flying',
|
||||||
|
'psychic',
|
||||||
|
'bug',
|
||||||
|
'rock',
|
||||||
|
'ghost',
|
||||||
|
'dragon',
|
||||||
|
'dark',
|
||||||
|
'steel',
|
||||||
|
'fairy',
|
||||||
];
|
];
|
||||||
|
|
||||||
let cache;
|
let cache;
|
||||||
|
|||||||
@ -41,9 +41,7 @@ settings.subscribe((s) => {
|
|||||||
if ((location.hash || '').startsWith('#/pokemon/')) rerender();
|
if ((location.hash || '').startsWith('#/pokemon/')) rerender();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
window
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => applyTheme());
|
||||||
.matchMedia('(prefers-color-scheme: dark)')
|
|
||||||
.addEventListener('change', () => applyTheme());
|
|
||||||
|
|
||||||
const app = document.getElementById('app');
|
const app = document.getElementById('app');
|
||||||
app.replaceChildren();
|
app.replaceChildren();
|
||||||
|
|||||||
@ -18,10 +18,18 @@ import { prefersReducedMotion } from './store/settings.js';
|
|||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{ pattern: /^#?\/?$/, view: () => DexGrid() },
|
{ pattern: /^#?\/?$/, view: () => DexGrid() },
|
||||||
{ pattern: /^#\/pokemon\/(\d+)$/, view: (m) => PokemonDetail(Number(m[1])), skeleton: detailSkeleton },
|
{
|
||||||
|
pattern: /^#\/pokemon\/(\d+)$/,
|
||||||
|
view: (m) => PokemonDetail(Number(m[1])),
|
||||||
|
skeleton: detailSkeleton,
|
||||||
|
},
|
||||||
{ pattern: /^#\/move\/(\d+)$/, view: (m) => MoveDetail(Number(m[1])), skeleton: lookupSkeleton },
|
{ pattern: /^#\/move\/(\d+)$/, view: (m) => MoveDetail(Number(m[1])), skeleton: lookupSkeleton },
|
||||||
{ pattern: /^#\/item\/(\d+)$/, view: (m) => ItemDetail(Number(m[1])), skeleton: lookupSkeleton },
|
{ pattern: /^#\/item\/(\d+)$/, view: (m) => ItemDetail(Number(m[1])), skeleton: lookupSkeleton },
|
||||||
{ pattern: /^#\/ability\/(\d+)$/, view: (m) => AbilityDetail(Number(m[1])), skeleton: lookupSkeleton },
|
{
|
||||||
|
pattern: /^#\/ability\/(\d+)$/,
|
||||||
|
view: (m) => AbilityDetail(Number(m[1])),
|
||||||
|
skeleton: lookupSkeleton,
|
||||||
|
},
|
||||||
{ pattern: /^#\/team$/, view: () => TeamView() },
|
{ pattern: /^#\/team$/, view: () => TeamView() },
|
||||||
{ pattern: /^#\/natures$/, view: () => NaturesView() },
|
{ pattern: /^#\/natures$/, view: () => NaturesView() },
|
||||||
{ pattern: /^#\/types$/, view: () => TypeChartView() },
|
{ pattern: /^#\/types$/, view: () => TypeChartView() },
|
||||||
|
|||||||
@ -34,10 +34,7 @@ export function createStore(key, initial) {
|
|||||||
return {
|
return {
|
||||||
get: () => state,
|
get: () => state,
|
||||||
set(patch) {
|
set(patch) {
|
||||||
state =
|
state = typeof patch === 'function' ? patch(state) : { ...state, ...patch };
|
||||||
typeof patch === 'function'
|
|
||||||
? patch(state)
|
|
||||||
: { ...state, ...patch };
|
|
||||||
save();
|
save();
|
||||||
for (const fn of subscribers) fn(state);
|
for (const fn of subscribers) fn(state);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -121,7 +121,8 @@ export function importFlags({ seen = [], caught = [], game } = {}) {
|
|||||||
const stamp = new Date().toISOString();
|
const stamp = new Date().toISOString();
|
||||||
const bucket = { ...(s.games[vgKey] || {}) };
|
const bucket = { ...(s.games[vgKey] || {}) };
|
||||||
for (const id of seen) bucket[id] = { ...(bucket[id] || GBLANK), seen: true, updatedAt: stamp };
|
for (const id of seen) bucket[id] = { ...(bucket[id] || GBLANK), seen: true, updatedAt: stamp };
|
||||||
for (const id of caught) bucket[id] = { ...(bucket[id] || GBLANK), seen: true, caught: true, updatedAt: stamp };
|
for (const id of caught)
|
||||||
|
bucket[id] = { ...(bucket[id] || GBLANK), seen: true, caught: true, updatedAt: stamp };
|
||||||
return { ...s, games: { ...s.games, [vgKey]: bucket } };
|
return { ...s, games: { ...s.games, [vgKey]: bucket } };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -140,10 +141,7 @@ export function replaceFlags({ seen = [], caught = [], game } = {}) {
|
|||||||
|
|
||||||
/** Aggregate seen/caught over a species list for one game (union when 'all'). */
|
/** Aggregate seen/caught over a species list for one game (union when 'all'). */
|
||||||
export function stats(speciesIds, vgKey = curGame()) {
|
export function stats(speciesIds, vgKey = curGame()) {
|
||||||
const get =
|
const get = vgKey === 'all' ? unionEntry : (id) => selection.get().games[vgKey]?.[id] || GBLANK;
|
||||||
vgKey === 'all'
|
|
||||||
? unionEntry
|
|
||||||
: (id) => selection.get().games[vgKey]?.[id] || GBLANK;
|
|
||||||
let seen = 0;
|
let seen = 0;
|
||||||
let caught = 0;
|
let caught = 0;
|
||||||
for (const id of speciesIds) {
|
for (const id of speciesIds) {
|
||||||
|
|||||||
@ -51,13 +51,10 @@ export function applyTheme(state = settings.get()) {
|
|||||||
if (meta) {
|
if (meta) {
|
||||||
const dark =
|
const dark =
|
||||||
DARKISH.has(theme) ||
|
DARKISH.has(theme) ||
|
||||||
((!theme || theme === 'system') &&
|
((!theme || theme === 'system') && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
||||||
meta.setAttribute(
|
meta.setAttribute(
|
||||||
'content',
|
'content',
|
||||||
dark
|
dark ? '#0b0b0c' : getComputedStyle(root).getPropertyValue('--accent').trim() || '#b3161a',
|
||||||
? '#0b0b0c'
|
|
||||||
: getComputedStyle(root).getPropertyValue('--accent').trim() || '#b3161a',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -65,7 +62,6 @@ export function applyTheme(state = settings.get()) {
|
|||||||
/** True if animations should be skipped — OS preference or the in-app override. */
|
/** True if animations should be skipped — OS preference or the in-app override. */
|
||||||
export function prefersReducedMotion() {
|
export function prefersReducedMotion() {
|
||||||
return (
|
return (
|
||||||
!!settings.get().reduceMotion ||
|
!!settings.get().reduceMotion || window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||||
window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,15 @@ export function addHunt({ speciesId, method, charm }) {
|
|||||||
shinyHunts.set((s) => ({
|
shinyHunts.set((s) => ({
|
||||||
...s,
|
...s,
|
||||||
hunts: [
|
hunts: [
|
||||||
{ id: uid(), speciesId, method, charm: !!charm, count: 0, notes: '', startedAt: new Date().toISOString() },
|
{
|
||||||
|
id: uid(),
|
||||||
|
speciesId,
|
||||||
|
method,
|
||||||
|
charm: !!charm,
|
||||||
|
count: 0,
|
||||||
|
notes: '',
|
||||||
|
startedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
...s.hunts,
|
...s.hunts,
|
||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
|
|||||||
@ -41,5 +41,8 @@ export function clearTeam() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function inTeam(id, form = null) {
|
export function inTeam(id, form = null) {
|
||||||
return team.get().members.map(slot).some((m) => sameSlot(m, { id, form: form || null }));
|
return team
|
||||||
|
.get()
|
||||||
|
.members.map(slot)
|
||||||
|
.some((m) => sameSlot(m, { id, form: form || null }));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -142,7 +142,9 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
transition: color 0.12s ease, background 0.12s ease;
|
transition:
|
||||||
|
color 0.12s ease,
|
||||||
|
background 0.12s ease;
|
||||||
}
|
}
|
||||||
.reflink:hover {
|
.reflink:hover {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
@ -226,24 +228,64 @@
|
|||||||
background: var(--type-normal);
|
background: var(--type-normal);
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
}
|
}
|
||||||
.type-chip[data-type="normal"] { background: var(--type-normal); }
|
.type-chip[data-type='normal'] {
|
||||||
.type-chip[data-type="fire"] { background: var(--type-fire); }
|
background: var(--type-normal);
|
||||||
.type-chip[data-type="water"] { background: var(--type-water); }
|
}
|
||||||
.type-chip[data-type="electric"] { background: var(--type-electric); color: #3a3a1a; }
|
.type-chip[data-type='fire'] {
|
||||||
.type-chip[data-type="grass"] { background: var(--type-grass); }
|
background: var(--type-fire);
|
||||||
.type-chip[data-type="ice"] { background: var(--type-ice); color: #1c3b38; }
|
}
|
||||||
.type-chip[data-type="fighting"] { background: var(--type-fighting); }
|
.type-chip[data-type='water'] {
|
||||||
.type-chip[data-type="poison"] { background: var(--type-poison); }
|
background: var(--type-water);
|
||||||
.type-chip[data-type="ground"] { background: var(--type-ground); }
|
}
|
||||||
.type-chip[data-type="flying"] { background: var(--type-flying); }
|
.type-chip[data-type='electric'] {
|
||||||
.type-chip[data-type="psychic"] { background: var(--type-psychic); }
|
background: var(--type-electric);
|
||||||
.type-chip[data-type="bug"] { background: var(--type-bug); }
|
color: #3a3a1a;
|
||||||
.type-chip[data-type="rock"] { background: var(--type-rock); color: #2e2a1c; }
|
}
|
||||||
.type-chip[data-type="ghost"] { background: var(--type-ghost); }
|
.type-chip[data-type='grass'] {
|
||||||
.type-chip[data-type="dragon"] { background: var(--type-dragon); }
|
background: var(--type-grass);
|
||||||
.type-chip[data-type="dark"] { background: var(--type-dark); }
|
}
|
||||||
.type-chip[data-type="steel"] { background: var(--type-steel); }
|
.type-chip[data-type='ice'] {
|
||||||
.type-chip[data-type="fairy"] { background: var(--type-fairy); color: #3d1f3b; }
|
background: var(--type-ice);
|
||||||
|
color: #1c3b38;
|
||||||
|
}
|
||||||
|
.type-chip[data-type='fighting'] {
|
||||||
|
background: var(--type-fighting);
|
||||||
|
}
|
||||||
|
.type-chip[data-type='poison'] {
|
||||||
|
background: var(--type-poison);
|
||||||
|
}
|
||||||
|
.type-chip[data-type='ground'] {
|
||||||
|
background: var(--type-ground);
|
||||||
|
}
|
||||||
|
.type-chip[data-type='flying'] {
|
||||||
|
background: var(--type-flying);
|
||||||
|
}
|
||||||
|
.type-chip[data-type='psychic'] {
|
||||||
|
background: var(--type-psychic);
|
||||||
|
}
|
||||||
|
.type-chip[data-type='bug'] {
|
||||||
|
background: var(--type-bug);
|
||||||
|
}
|
||||||
|
.type-chip[data-type='rock'] {
|
||||||
|
background: var(--type-rock);
|
||||||
|
color: #2e2a1c;
|
||||||
|
}
|
||||||
|
.type-chip[data-type='ghost'] {
|
||||||
|
background: var(--type-ghost);
|
||||||
|
}
|
||||||
|
.type-chip[data-type='dragon'] {
|
||||||
|
background: var(--type-dragon);
|
||||||
|
}
|
||||||
|
.type-chip[data-type='dark'] {
|
||||||
|
background: var(--type-dark);
|
||||||
|
}
|
||||||
|
.type-chip[data-type='steel'] {
|
||||||
|
background: var(--type-steel);
|
||||||
|
}
|
||||||
|
.type-chip[data-type='fairy'] {
|
||||||
|
background: var(--type-fairy);
|
||||||
|
color: #3d1f3b;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- Dex feed header ------------------------------------- */
|
/* ---------- Dex feed header ------------------------------------- */
|
||||||
.feed-head {
|
.feed-head {
|
||||||
@ -477,13 +519,21 @@
|
|||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
content-visibility: auto;
|
content-visibility: auto;
|
||||||
contain-intrinsic-size: auto 250px;
|
contain-intrinsic-size: auto 250px;
|
||||||
transition: transform 0.14s var(--ease-spring), box-shadow 0.14s ease;
|
transition:
|
||||||
|
transform 0.14s var(--ease-spring),
|
||||||
|
box-shadow 0.14s ease;
|
||||||
animation: cardIn 0.4s var(--ease-spring) backwards;
|
animation: cardIn 0.4s var(--ease-spring) backwards;
|
||||||
animation-delay: calc(var(--i, 0) * 18ms);
|
animation-delay: calc(var(--i, 0) * 18ms);
|
||||||
}
|
}
|
||||||
@keyframes cardIn {
|
@keyframes cardIn {
|
||||||
from { opacity: 0; transform: translateY(10px); }
|
from {
|
||||||
to { opacity: 1; transform: none; }
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.card:hover {
|
.card:hover {
|
||||||
transform: translateY(-4px);
|
transform: translateY(-4px);
|
||||||
@ -532,11 +582,11 @@
|
|||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
filter: drop-shadow(0 8px 12px rgba(0, 0, 0, 0.28));
|
filter: drop-shadow(0 8px 12px rgba(0, 0, 0, 0.28));
|
||||||
}
|
}
|
||||||
.card[data-sprite="default"] .card__art .sprite,
|
.card[data-sprite='default'] .card__art .sprite,
|
||||||
.card[data-sprite="game"] .card__art .sprite {
|
.card[data-sprite='game'] .card__art .sprite {
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
}
|
}
|
||||||
.card[data-sprite="game"] .card__art .sprite {
|
.card[data-sprite='game'] .card__art .sprite {
|
||||||
width: 92px;
|
width: 92px;
|
||||||
height: 92px;
|
height: 92px;
|
||||||
}
|
}
|
||||||
@ -548,7 +598,9 @@
|
|||||||
padding: 5px;
|
padding: 5px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: #f7f7f5;
|
background: #f7f7f5;
|
||||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08), 0 2px 6px rgba(0, 0, 0, 0.14);
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgba(0, 0, 0, 0.08),
|
||||||
|
0 2px 6px rgba(0, 0, 0, 0.14);
|
||||||
filter: none;
|
filter: none;
|
||||||
}
|
}
|
||||||
.card__body {
|
.card__body {
|
||||||
@ -606,15 +658,15 @@
|
|||||||
background: rgba(255, 255, 255, 0.75);
|
background: rgba(255, 255, 255, 0.75);
|
||||||
backdrop-filter: blur(3px);
|
backdrop-filter: blur(3px);
|
||||||
}
|
}
|
||||||
.card__caught[aria-pressed="true"] {
|
.card__caught[aria-pressed='true'] {
|
||||||
background: var(--good);
|
background: var(--good);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
.card__caught[aria-pressed="true"]::before {
|
.card__caught[aria-pressed='true']::before {
|
||||||
content: "✓";
|
content: '✓';
|
||||||
}
|
}
|
||||||
.card__caught[aria-pressed="false"]::before {
|
.card__caught[aria-pressed='false']::before {
|
||||||
content: "+";
|
content: '+';
|
||||||
}
|
}
|
||||||
.card.is-caught {
|
.card.is-caught {
|
||||||
border-color: color-mix(in srgb, var(--good) 45%, var(--border));
|
border-color: color-mix(in srgb, var(--good) 45%, var(--border));
|
||||||
@ -677,7 +729,9 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
transition: transform 0.14s var(--ease-spring), border-color 0.14s ease;
|
transition:
|
||||||
|
transform 0.14s var(--ease-spring),
|
||||||
|
border-color 0.14s ease;
|
||||||
}
|
}
|
||||||
.gamecard:hover {
|
.gamecard:hover {
|
||||||
transform: translateY(-3px);
|
transform: translateY(-3px);
|
||||||
@ -710,7 +764,7 @@
|
|||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
}
|
}
|
||||||
.gamecard--cover::after {
|
.gamecard--cover::after {
|
||||||
content: "";
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.28), transparent 55%);
|
background: linear-gradient(to top, rgba(0, 0, 0, 0.28), transparent 55%);
|
||||||
@ -724,7 +778,9 @@
|
|||||||
}
|
}
|
||||||
.gamecard--cover.is-active {
|
.gamecard--cover.is-active {
|
||||||
background: linear-gradient(105deg, var(--g1) 0%, var(--g1) 42%, var(--g2) 58%, var(--g2) 100%);
|
background: linear-gradient(105deg, var(--g1) 0%, var(--g1) 42%, var(--g2) 58%, var(--g2) 100%);
|
||||||
box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--accent);
|
box-shadow:
|
||||||
|
0 0 0 2px var(--surface),
|
||||||
|
0 0 0 4px var(--accent);
|
||||||
}
|
}
|
||||||
.gamecard--cover .gamecard__name {
|
.gamecard--cover .gamecard__name {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
@ -774,10 +830,16 @@
|
|||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: -2;
|
z-index: -2;
|
||||||
background:
|
background:
|
||||||
radial-gradient(120% 80% at 80% 0%, color-mix(in srgb, var(--type-2) 55%, transparent), transparent 60%),
|
radial-gradient(
|
||||||
linear-gradient(160deg,
|
120% 80% at 80% 0%,
|
||||||
|
color-mix(in srgb, var(--type-2) 55%, transparent),
|
||||||
|
transparent 60%
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
160deg,
|
||||||
color-mix(in srgb, var(--type-main) 92%, #fff 8%) 0%,
|
color-mix(in srgb, var(--type-main) 92%, #fff 8%) 0%,
|
||||||
color-mix(in srgb, var(--type-main) 62%, #000 38%) 100%);
|
color-mix(in srgb, var(--type-main) 62%, #000 38%) 100%
|
||||||
|
);
|
||||||
}
|
}
|
||||||
.phero__ball {
|
.phero__ball {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -791,7 +853,7 @@
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
.phero__ball::before {
|
.phero__ball::before {
|
||||||
content: "";
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: -20px;
|
left: -20px;
|
||||||
right: -20px;
|
right: -20px;
|
||||||
@ -801,7 +863,7 @@
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
.phero__ball::after {
|
.phero__ball::after {
|
||||||
content: "";
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
@ -912,15 +974,23 @@
|
|||||||
padding: 14px;
|
padding: 14px;
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
background: #f7f7f5;
|
background: #f7f7f5;
|
||||||
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.22), inset 0 0 0 1px rgba(0, 0, 0, 0.08);
|
box-shadow:
|
||||||
|
0 10px 24px rgba(0, 0, 0, 0.22),
|
||||||
|
inset 0 0 0 1px rgba(0, 0, 0, 0.08);
|
||||||
}
|
}
|
||||||
.phero__art--boxed img {
|
.phero__art--boxed img {
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
filter: none;
|
filter: none;
|
||||||
}
|
}
|
||||||
@keyframes pop {
|
@keyframes pop {
|
||||||
from { transform: scale(0.8); opacity: 0; }
|
from {
|
||||||
to { transform: scale(1); opacity: 1; }
|
transform: scale(0.8);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.phero__nav {
|
.phero__nav {
|
||||||
width: 44px;
|
width: 44px;
|
||||||
@ -1036,9 +1106,15 @@
|
|||||||
animation: pnote-fade 1.6s ease forwards;
|
animation: pnote-fade 1.6s ease forwards;
|
||||||
}
|
}
|
||||||
@keyframes pnote-fade {
|
@keyframes pnote-fade {
|
||||||
0% { opacity: 1; }
|
0% {
|
||||||
70% { opacity: 1; }
|
opacity: 1;
|
||||||
100% { opacity: 0; }
|
}
|
||||||
|
70% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.pnote__input {
|
.pnote__input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -1092,8 +1168,14 @@
|
|||||||
animation: fade 0.25s ease;
|
animation: fade 0.25s ease;
|
||||||
}
|
}
|
||||||
@keyframes fade {
|
@keyframes fade {
|
||||||
from { opacity: 0; transform: translateY(4px); }
|
from {
|
||||||
to { opacity: 1; transform: none; }
|
opacity: 0;
|
||||||
|
transform: translateY(4px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.ppanel__flavor {
|
.ppanel__flavor {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
@ -1131,8 +1213,14 @@
|
|||||||
animation: flavorSlide 0.28s var(--ease-spring);
|
animation: flavorSlide 0.28s var(--ease-spring);
|
||||||
}
|
}
|
||||||
@keyframes flavorSlide {
|
@keyframes flavorSlide {
|
||||||
from { opacity: 0; transform: translateX(14px); }
|
from {
|
||||||
to { opacity: 1; transform: none; }
|
opacity: 0;
|
||||||
|
transform: translateX(14px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.flavor__cap {
|
.flavor__cap {
|
||||||
display: block;
|
display: block;
|
||||||
@ -1221,12 +1309,24 @@
|
|||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
transition: width 0.7s var(--ease-spring);
|
transition: width 0.7s var(--ease-spring);
|
||||||
}
|
}
|
||||||
.statbar__fill[data-stat="hp"] { background: #ff6b6b; }
|
.statbar__fill[data-stat='hp'] {
|
||||||
.statbar__fill[data-stat="attack"] { background: #ffa94d; }
|
background: #ff6b6b;
|
||||||
.statbar__fill[data-stat="defense"] { background: #ffd43b; }
|
}
|
||||||
.statbar__fill[data-stat="special-attack"] { background: #74c0fc; }
|
.statbar__fill[data-stat='attack'] {
|
||||||
.statbar__fill[data-stat="special-defense"] { background: #63e6be; }
|
background: #ffa94d;
|
||||||
.statbar__fill[data-stat="speed"] { background: #da77f2; }
|
}
|
||||||
|
.statbar__fill[data-stat='defense'] {
|
||||||
|
background: #ffd43b;
|
||||||
|
}
|
||||||
|
.statbar__fill[data-stat='special-attack'] {
|
||||||
|
background: #74c0fc;
|
||||||
|
}
|
||||||
|
.statbar__fill[data-stat='special-defense'] {
|
||||||
|
background: #63e6be;
|
||||||
|
}
|
||||||
|
.statbar__fill[data-stat='speed'] {
|
||||||
|
background: #da77f2;
|
||||||
|
}
|
||||||
.statbar--total {
|
.statbar--total {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
@ -1378,7 +1478,7 @@
|
|||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
transition: transform 0.15s ease;
|
transition: transform 0.15s ease;
|
||||||
}
|
}
|
||||||
.moverow[aria-expanded="true"] .moverow__chev {
|
.moverow[aria-expanded='true'] .moverow__chev {
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
.movebody {
|
.movebody {
|
||||||
@ -1427,9 +1527,15 @@
|
|||||||
letter-spacing: 0.03em;
|
letter-spacing: 0.03em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
.moverow__cat[data-cat="physical"] { color: #e0762f; }
|
.moverow__cat[data-cat='physical'] {
|
||||||
.moverow__cat[data-cat="special"] { color: #4b8fd6; }
|
color: #e0762f;
|
||||||
.moverow__cat[data-cat="status"] { color: #8a8f98; }
|
}
|
||||||
|
.moverow__cat[data-cat='special'] {
|
||||||
|
color: #4b8fd6;
|
||||||
|
}
|
||||||
|
.moverow__cat[data-cat='status'] {
|
||||||
|
color: #8a8f98;
|
||||||
|
}
|
||||||
.moverow__stat {
|
.moverow__stat {
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@ -1530,7 +1636,9 @@
|
|||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
transition: transform 0.12s var(--ease-spring), border-color 0.12s ease;
|
transition:
|
||||||
|
transform 0.12s var(--ease-spring),
|
||||||
|
border-color 0.12s ease;
|
||||||
}
|
}
|
||||||
.search__row:hover {
|
.search__row:hover {
|
||||||
transform: translateX(3px);
|
transform: translateX(3px);
|
||||||
@ -1704,7 +1812,9 @@
|
|||||||
}
|
}
|
||||||
.swatch.is-on {
|
.swatch.is-on {
|
||||||
border-color: var(--text);
|
border-color: var(--text);
|
||||||
box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--sw);
|
box-shadow:
|
||||||
|
0 0 0 2px var(--bg),
|
||||||
|
0 0 0 4px var(--sw);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Toast ------------------------------------------- */
|
/* ---------- Toast ------------------------------------------- */
|
||||||
@ -1802,7 +1912,7 @@
|
|||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
.gsheet__head::before {
|
.gsheet__head::before {
|
||||||
content: "";
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 6px;
|
top: 6px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@ -1868,8 +1978,14 @@
|
|||||||
animation: viewIn 0.26s ease both;
|
animation: viewIn 0.26s ease both;
|
||||||
}
|
}
|
||||||
@keyframes viewIn {
|
@keyframes viewIn {
|
||||||
from { opacity: 0; transform: translateY(6px); }
|
from {
|
||||||
to { opacity: 1; transform: none; }
|
opacity: 0;
|
||||||
|
transform: translateY(6px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
::view-transition-old(root),
|
::view-transition-old(root),
|
||||||
@ -1894,8 +2010,12 @@
|
|||||||
animation: shimmer 1.25s linear infinite;
|
animation: shimmer 1.25s linear infinite;
|
||||||
}
|
}
|
||||||
@keyframes shimmer {
|
@keyframes shimmer {
|
||||||
from { background-position: 220% 0; }
|
from {
|
||||||
to { background-position: -120% 0; }
|
background-position: 220% 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
background-position: -120% 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.sk-hero {
|
.sk-hero {
|
||||||
background: var(--surface-2);
|
background: var(--surface-2);
|
||||||
@ -1963,7 +2083,7 @@
|
|||||||
animation: viewIn 0.25s ease both;
|
animation: viewIn 0.25s ease both;
|
||||||
}
|
}
|
||||||
.netpill::before {
|
.netpill::before {
|
||||||
content: "";
|
content: '';
|
||||||
width: 7px;
|
width: 7px;
|
||||||
height: 7px;
|
height: 7px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
@ -2103,10 +2223,10 @@
|
|||||||
transition: transform 0.15s ease;
|
transition: transform 0.15s ease;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
.feed-tools__toggle[aria-expanded="true"] .feed-tools__chev {
|
.feed-tools__toggle[aria-expanded='true'] .feed-tools__chev {
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
.feed-tools__toggle[data-active]:not([data-active=""])::before {
|
.feed-tools__toggle[data-active]:not([data-active=''])::before {
|
||||||
content: attr(data-active);
|
content: attr(data-active);
|
||||||
min-width: 17px;
|
min-width: 17px;
|
||||||
height: 17px;
|
height: 17px;
|
||||||
@ -2314,11 +2434,24 @@
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
.cov__cell.m4 { background: color-mix(in srgb, var(--danger) 60%, transparent); color: #fff; }
|
.cov__cell.m4 {
|
||||||
.cov__cell.m2 { background: color-mix(in srgb, var(--danger) 28%, transparent); }
|
background: color-mix(in srgb, var(--danger) 60%, transparent);
|
||||||
.cov__cell.m05 { background: color-mix(in srgb, var(--good) 24%, transparent); }
|
color: #fff;
|
||||||
.cov__cell.m025 { background: color-mix(in srgb, var(--good) 48%, transparent); color: #fff; }
|
}
|
||||||
.cov__cell.m0 { background: var(--surface-2); color: var(--text-dim); }
|
.cov__cell.m2 {
|
||||||
|
background: color-mix(in srgb, var(--danger) 28%, transparent);
|
||||||
|
}
|
||||||
|
.cov__cell.m05 {
|
||||||
|
background: color-mix(in srgb, var(--good) 24%, transparent);
|
||||||
|
}
|
||||||
|
.cov__cell.m025 {
|
||||||
|
background: color-mix(in srgb, var(--good) 48%, transparent);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.cov__cell.m0 {
|
||||||
|
background: var(--surface-2);
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
.cov__row--threat {
|
.cov__row--threat {
|
||||||
background: color-mix(in srgb, var(--danger) 12%, transparent);
|
background: color-mix(in srgb, var(--danger) 12%, transparent);
|
||||||
box-shadow: inset 3px 0 0 var(--danger);
|
box-shadow: inset 3px 0 0 var(--danger);
|
||||||
@ -2965,7 +3098,9 @@
|
|||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
filter: grayscale(1);
|
filter: grayscale(1);
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
transition: filter 0.15s, opacity 0.15s;
|
transition:
|
||||||
|
filter 0.15s,
|
||||||
|
opacity 0.15s;
|
||||||
}
|
}
|
||||||
.formdex__tile.is-caught .formdex__sprite {
|
.formdex__tile.is-caught .formdex__sprite {
|
||||||
filter: none;
|
filter: none;
|
||||||
@ -2996,7 +3131,9 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(0.6);
|
transform: scale(0.6);
|
||||||
transition: opacity 0.15s, transform 0.15s;
|
transition:
|
||||||
|
opacity 0.15s,
|
||||||
|
transform 0.15s;
|
||||||
}
|
}
|
||||||
.formdex__tile.is-caught .formdex__check {
|
.formdex__tile.is-caught .formdex__check {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@ -3231,8 +3368,12 @@
|
|||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
.prog__key--caught { background: var(--good); }
|
.prog__key--caught {
|
||||||
.prog__key--seen { background: color-mix(in srgb, var(--accent) 35%, transparent); }
|
background: var(--good);
|
||||||
|
}
|
||||||
|
.prog__key--seen {
|
||||||
|
background: color-mix(in srgb, var(--accent) 35%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- Breeding helper --------------------------------------- */
|
/* ---- Breeding helper --------------------------------------- */
|
||||||
.breedview__body {
|
.breedview__body {
|
||||||
@ -3403,15 +3544,44 @@
|
|||||||
/* ================= Micro-interactions ============================ */
|
/* ================= Micro-interactions ============================ */
|
||||||
/* Tactile press feedback — low specificity so any element's own
|
/* Tactile press feedback — low specificity so any element's own
|
||||||
:active / transition wins if it has one. */
|
:active / transition wins if it has one. */
|
||||||
:where(.button, .seg__btn, .feed-chip, .breed__chip, .formbar__pill,
|
:where(
|
||||||
.flavor__pill, .lineup__add, .lineup__clear, .shiny__step, .prog__row,
|
.button,
|
||||||
.tchart__row, .tchart__col, .gamecard, .picker__row, .weakspot__chip) {
|
.seg__btn,
|
||||||
transition: transform 0.09s var(--ease-spring), background-color 0.16s ease,
|
.feed-chip,
|
||||||
color 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease;
|
.breed__chip,
|
||||||
|
.formbar__pill,
|
||||||
|
.flavor__pill,
|
||||||
|
.lineup__add,
|
||||||
|
.lineup__clear,
|
||||||
|
.shiny__step,
|
||||||
|
.prog__row,
|
||||||
|
.tchart__row,
|
||||||
|
.tchart__col,
|
||||||
|
.gamecard,
|
||||||
|
.picker__row,
|
||||||
|
.weakspot__chip
|
||||||
|
) {
|
||||||
|
transition:
|
||||||
|
transform 0.09s var(--ease-spring),
|
||||||
|
background-color 0.16s ease,
|
||||||
|
color 0.16s ease,
|
||||||
|
box-shadow 0.16s ease,
|
||||||
|
border-color 0.16s ease;
|
||||||
}
|
}
|
||||||
:where(.button, .seg__btn, .feed-chip, .breed__chip, .formbar__pill,
|
:where(
|
||||||
.flavor__pill, .lineup__add, .shiny__step, .prog__row, .tchart__row,
|
.button,
|
||||||
.tchart__col, .gamecard):active {
|
.seg__btn,
|
||||||
|
.feed-chip,
|
||||||
|
.breed__chip,
|
||||||
|
.formbar__pill,
|
||||||
|
.flavor__pill,
|
||||||
|
.lineup__add,
|
||||||
|
.shiny__step,
|
||||||
|
.prog__row,
|
||||||
|
.tchart__row,
|
||||||
|
.tchart__col,
|
||||||
|
.gamecard
|
||||||
|
):active {
|
||||||
transform: scale(0.96);
|
transform: scale(0.96);
|
||||||
}
|
}
|
||||||
.card:active {
|
.card:active {
|
||||||
@ -3429,8 +3599,13 @@
|
|||||||
animation: heroFloat 4.2s ease-in-out infinite;
|
animation: heroFloat 4.2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
@keyframes heroFloat {
|
@keyframes heroFloat {
|
||||||
0%, 100% { transform: translateY(0); }
|
0%,
|
||||||
50% { transform: translateY(-6px); }
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-6px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sliding thumb behind the active segment (JS sets --seg-i / --seg-n). */
|
/* Sliding thumb behind the active segment (JS sets --seg-i / --seg-n). */
|
||||||
@ -3446,7 +3621,9 @@
|
|||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
transition: transform 0.28s var(--ease-spring), width 0.28s var(--ease-spring);
|
transition:
|
||||||
|
transform 0.28s var(--ease-spring),
|
||||||
|
width 0.28s var(--ease-spring);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
.seg__btn {
|
.seg__btn {
|
||||||
@ -3482,7 +3659,9 @@
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
transform: translateY(12px) scale(0.97);
|
transform: translateY(12px) scale(0.97);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: transform 0.2s var(--ease-spring), opacity 0.2s ease;
|
transition:
|
||||||
|
transform 0.2s var(--ease-spring),
|
||||||
|
opacity 0.2s ease;
|
||||||
}
|
}
|
||||||
.dialog-backdrop.is-open .dialog {
|
.dialog-backdrop.is-open .dialog {
|
||||||
transform: none;
|
transform: none;
|
||||||
|
|||||||
@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
--shadow: 0 1px 2px rgba(0, 0, 0, 0.05), 0 10px 30px rgba(0, 0, 0, 0.08);
|
--shadow: 0 1px 2px rgba(0, 0, 0, 0.05), 0 10px 30px rgba(0, 0, 0, 0.08);
|
||||||
--shadow-lg: 0 20px 50px -12px rgba(0, 0, 0, 0.28);
|
--shadow-lg: 0 20px 50px -12px rgba(0, 0, 0, 0.28);
|
||||||
--font: "Poppins", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
--font: 'Poppins', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
||||||
--ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);
|
--ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);
|
||||||
|
|
||||||
/* Pokémon type colors */
|
/* Pokémon type colors */
|
||||||
@ -62,7 +62,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="dark"] {
|
:root[data-theme='dark'] {
|
||||||
--bg: #0b0b0c;
|
--bg: #0b0b0c;
|
||||||
--surface: #17171a;
|
--surface: #17171a;
|
||||||
--surface-2: #202024;
|
--surface-2: #202024;
|
||||||
@ -75,7 +75,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Pure black — for OLED screens. */
|
/* Pure black — for OLED screens. */
|
||||||
:root[data-theme="black"] {
|
:root[data-theme='black'] {
|
||||||
--bg: #000000;
|
--bg: #000000;
|
||||||
--surface: #0b0b0c;
|
--surface: #0b0b0c;
|
||||||
--surface-2: #17171a;
|
--surface-2: #17171a;
|
||||||
@ -89,7 +89,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Sepia — a warm, low-glare light theme. */
|
/* Sepia — a warm, low-glare light theme. */
|
||||||
:root[data-theme="sepia"] {
|
:root[data-theme='sepia'] {
|
||||||
--bg: #f2e8d5;
|
--bg: #f2e8d5;
|
||||||
--surface: #fbf4e4;
|
--surface: #fbf4e4;
|
||||||
--surface-2: #e9dbbe;
|
--surface-2: #e9dbbe;
|
||||||
@ -101,11 +101,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Accent overrides — layered on top of any theme. Red is the default. */
|
/* Accent overrides — layered on top of any theme. Red is the default. */
|
||||||
:root[data-accent="blue"] { --accent: #2f6fed; --accent-text: #ffffff; }
|
:root[data-accent='blue'] {
|
||||||
:root[data-accent="green"] { --accent: #1f9d57; --accent-text: #ffffff; }
|
--accent: #2f6fed;
|
||||||
:root[data-accent="amber"] { --accent: #d98a00; --accent-text: #1a1200; }
|
--accent-text: #ffffff;
|
||||||
:root[data-accent="violet"] { --accent: #7b52e0; --accent-text: #ffffff; }
|
}
|
||||||
:root[data-accent="rose"] { --accent: #e0417a; --accent-text: #ffffff; }
|
:root[data-accent='green'] {
|
||||||
|
--accent: #1f9d57;
|
||||||
|
--accent-text: #ffffff;
|
||||||
|
}
|
||||||
|
:root[data-accent='amber'] {
|
||||||
|
--accent: #d98a00;
|
||||||
|
--accent-text: #1a1200;
|
||||||
|
}
|
||||||
|
:root[data-accent='violet'] {
|
||||||
|
--accent: #7b52e0;
|
||||||
|
--accent-text: #ffffff;
|
||||||
|
}
|
||||||
|
:root[data-accent='rose'] {
|
||||||
|
--accent: #e0417a;
|
||||||
|
--accent-text: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|||||||
@ -101,9 +101,7 @@ export async function AbilityDetail(id) {
|
|||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
mons.length > 24 ? filterInput : null,
|
mons.length > 24 ? filterInput : null,
|
||||||
mons.length
|
mons.length ? grid : el('p', { class: 'detail__muted' }, 'None on record.'),
|
||||||
? grid
|
|
||||||
: el('p', { class: 'detail__muted' }, 'None on record.'),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return view;
|
return view;
|
||||||
|
|||||||
@ -9,9 +9,15 @@ import { openPokemonPicker } from '../components/PokemonPicker.js';
|
|||||||
import { segThumb } from '../lib/anim.js';
|
import { segThumb } from '../lib/anim.js';
|
||||||
|
|
||||||
const EGG_LABEL = {
|
const EGG_LABEL = {
|
||||||
water1: 'Water 1', water2: 'Water 2', water3: 'Water 3',
|
water1: 'Water 1',
|
||||||
ground: 'Field', humanshape: 'Human-Like', indeterminate: 'Amorphous',
|
water2: 'Water 2',
|
||||||
plant: 'Grass', 'no-eggs': 'Undiscovered', ditto: 'Ditto',
|
water3: 'Water 3',
|
||||||
|
ground: 'Field',
|
||||||
|
humanshape: 'Human-Like',
|
||||||
|
indeterminate: 'Amorphous',
|
||||||
|
plant: 'Grass',
|
||||||
|
'no-eggs': 'Undiscovered',
|
||||||
|
ditto: 'Ditto',
|
||||||
};
|
};
|
||||||
const eggLabel = (g) => EGG_LABEL[g] || prettify(g);
|
const eggLabel = (g) => EGG_LABEL[g] || prettify(g);
|
||||||
const idFromUrl = (u) => Number(u.replace(/\/$/, '').split('/').pop());
|
const idFromUrl = (u) => Number(u.replace(/\/$/, '').split('/').pop());
|
||||||
@ -57,7 +63,14 @@ export async function BreedingView() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const setThumb = segThumb(seg, SEG.length, Math.max(0, SEG.findIndex(([id]) => id === mode)));
|
const setThumb = segThumb(
|
||||||
|
seg,
|
||||||
|
SEG.length,
|
||||||
|
Math.max(
|
||||||
|
0,
|
||||||
|
SEG.findIndex(([id]) => id === mode),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const body = el('div', { class: 'breedview__body' });
|
const body = el('div', { class: 'breedview__body' });
|
||||||
|
|
||||||
@ -125,7 +138,12 @@ export async function BreedingView() {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
target
|
target
|
||||||
? el('span', {}, Sprite(target.id, { style, size: 40, alt: target.name }), ` ${prettify(target.name)}`)
|
? el(
|
||||||
|
'span',
|
||||||
|
{},
|
||||||
|
Sprite(target.id, { style, size: 40, alt: target.name }),
|
||||||
|
` ${prettify(target.name)}`,
|
||||||
|
)
|
||||||
: 'Choose a Pokémon…',
|
: 'Choose a Pokémon…',
|
||||||
);
|
);
|
||||||
body.append(slot);
|
body.append(slot);
|
||||||
@ -133,7 +151,13 @@ export async function BreedingView() {
|
|||||||
|
|
||||||
const tGroups = new Set(target.eggGroups || []);
|
const tGroups = new Set(target.eggGroups || []);
|
||||||
if (tGroups.has('no-eggs')) {
|
if (tGroups.has('no-eggs')) {
|
||||||
body.append(el('p', { class: 'detail__muted' }, `${prettify(target.name)} is in the Undiscovered egg group — it can’t be bred.`));
|
body.append(
|
||||||
|
el(
|
||||||
|
'p',
|
||||||
|
{ class: 'detail__muted' },
|
||||||
|
`${prettify(target.name)} is in the Undiscovered egg group — it can’t be bred.`,
|
||||||
|
),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const isDitto = tGroups.has('ditto');
|
const isDitto = tGroups.has('ditto');
|
||||||
@ -141,7 +165,9 @@ export async function BreedingView() {
|
|||||||
|
|
||||||
let list;
|
let list;
|
||||||
if (isDitto) {
|
if (isDitto) {
|
||||||
list = snap.species.filter((s) => !s.eggGroups.includes('no-eggs') && !s.eggGroups.includes('ditto'));
|
list = snap.species.filter(
|
||||||
|
(s) => !s.eggGroups.includes('no-eggs') && !s.eggGroups.includes('ditto'),
|
||||||
|
);
|
||||||
} else if (genderless) {
|
} else if (genderless) {
|
||||||
list = snap.species.filter((s) => (s.eggGroups || []).includes('ditto'));
|
list = snap.species.filter((s) => (s.eggGroups || []).includes('ditto'));
|
||||||
} else {
|
} else {
|
||||||
@ -168,12 +194,17 @@ export async function BreedingView() {
|
|||||||
if (!prev) break;
|
if (!prev) break;
|
||||||
base = prev;
|
base = prev;
|
||||||
}
|
}
|
||||||
const eggMovesBox = el('div', { class: 'breed__eggmoves' }, el('p', { class: 'detail__muted' }, 'Loading egg moves…'));
|
const eggMovesBox = el(
|
||||||
|
'div',
|
||||||
|
{ class: 'breed__eggmoves' },
|
||||||
|
el('p', { class: 'detail__muted' }, 'Loading egg moves…'),
|
||||||
|
);
|
||||||
getPokemon(base.id)
|
getPokemon(base.id)
|
||||||
.then((pk) => {
|
.then((pk) => {
|
||||||
const eggMoves = pk.moves.filter((m) =>
|
const eggMoves = pk.moves.filter((m) =>
|
||||||
m.version_group_details.some(
|
m.version_group_details.some(
|
||||||
(d) => d.move_learn_method.name === 'egg' && (vg === 'all' || d.version_group.name === vg),
|
(d) =>
|
||||||
|
d.move_learn_method.name === 'egg' && (vg === 'all' || d.version_group.name === vg),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
clear(eggMovesBox);
|
clear(eggMovesBox);
|
||||||
@ -196,12 +227,20 @@ export async function BreedingView() {
|
|||||||
'div',
|
'div',
|
||||||
{ class: 'breed__eggmove-list' },
|
{ class: 'breed__eggmove-list' },
|
||||||
...eggMoves.map((m) =>
|
...eggMoves.map((m) =>
|
||||||
el('a', { class: 'link', href: `#/move/${idFromUrl(m.move.url)}` }, m.move.name.replace(/-/g, ' ')),
|
el(
|
||||||
|
'a',
|
||||||
|
{ class: 'link', href: `#/move/${idFromUrl(m.move.url)}` },
|
||||||
|
m.move.name.replace(/-/g, ' '),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch(() => clear(eggMovesBox).append(el('p', { class: 'detail__muted' }, 'Egg moves unavailable offline.')));
|
.catch(() =>
|
||||||
|
clear(eggMovesBox).append(
|
||||||
|
el('p', { class: 'detail__muted' }, 'Egg moves unavailable offline.'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
body.append(
|
body.append(
|
||||||
el('p', { class: 'breed__note' }, note),
|
el('p', { class: 'breed__note' }, note),
|
||||||
|
|||||||
@ -95,7 +95,11 @@ export async function CompareView() {
|
|||||||
body.append(
|
body.append(
|
||||||
mem.length >= 2
|
mem.length >= 2
|
||||||
? CompareTable(mem, { gen, style })
|
? CompareTable(mem, { gen, style })
|
||||||
: el('p', { class: 'detail__muted' }, 'Add two or more Pokémon to compare their types and base stats.'),
|
: el(
|
||||||
|
'p',
|
||||||
|
{ class: 'detail__muted' },
|
||||||
|
'Add two or more Pokémon to compare their types and base stats.',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,11 +109,7 @@ export async function CompareView() {
|
|||||||
'header',
|
'header',
|
||||||
{ class: 'view__header' },
|
{ class: 'view__header' },
|
||||||
el('h1', {}, 'Compare'),
|
el('h1', {}, 'Compare'),
|
||||||
el(
|
el('p', {}, vg ? `Types shown for ${vg.name}.` : 'Types shown for the newest games.'),
|
||||||
'p',
|
|
||||||
{},
|
|
||||||
vg ? `Types shown for ${vg.name}.` : 'Types shown for the newest games.',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
slots,
|
slots,
|
||||||
body,
|
body,
|
||||||
|
|||||||
@ -10,8 +10,24 @@ import { Sprite } from '../components/Sprite.js';
|
|||||||
import { typesForGen } from '../lib/type-resolve.js';
|
import { typesForGen } from '../lib/type-resolve.js';
|
||||||
|
|
||||||
const TYPES = [
|
const TYPES = [
|
||||||
'normal', 'fire', 'water', 'electric', 'grass', 'ice', 'fighting', 'poison',
|
'normal',
|
||||||
'ground', 'flying', 'psychic', 'bug', 'rock', 'ghost', 'dragon', 'dark', 'steel', 'fairy',
|
'fire',
|
||||||
|
'water',
|
||||||
|
'electric',
|
||||||
|
'grass',
|
||||||
|
'ice',
|
||||||
|
'fighting',
|
||||||
|
'poison',
|
||||||
|
'ground',
|
||||||
|
'flying',
|
||||||
|
'psychic',
|
||||||
|
'bug',
|
||||||
|
'rock',
|
||||||
|
'ghost',
|
||||||
|
'dragon',
|
||||||
|
'dark',
|
||||||
|
'steel',
|
||||||
|
'fairy',
|
||||||
];
|
];
|
||||||
|
|
||||||
const FILTERS = [
|
const FILTERS = [
|
||||||
@ -47,30 +63,55 @@ const SEARCH_ICON =
|
|||||||
function sortValue(row, key) {
|
function sortValue(row, key) {
|
||||||
const sp = row.species;
|
const sp = row.species;
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'dex': return row.number ?? sp.id;
|
case 'dex':
|
||||||
case 'name': return sp.name;
|
return row.number ?? sp.id;
|
||||||
case 'bst': return sp.bst ?? 0;
|
case 'name':
|
||||||
case 'hp': case 'atk': case 'def': case 'spa': case 'spd': case 'spe':
|
return sp.name;
|
||||||
|
case 'bst':
|
||||||
|
return sp.bst ?? 0;
|
||||||
|
case 'hp':
|
||||||
|
case 'atk':
|
||||||
|
case 'def':
|
||||||
|
case 'spa':
|
||||||
|
case 'spd':
|
||||||
|
case 'spe':
|
||||||
return sp.stats?.[key] ?? 0;
|
return sp.stats?.[key] ?? 0;
|
||||||
case 'height': return sp.height ?? 0;
|
case 'height':
|
||||||
case 'weight': return sp.weight ?? 0;
|
return sp.height ?? 0;
|
||||||
case 'baseExp': return sp.baseExp ?? 0;
|
case 'weight':
|
||||||
case 'catchrate': return sp.captureRate ?? 0;
|
return sp.weight ?? 0;
|
||||||
case 'caught': return gameEntry(sp.id).updatedAt || '';
|
case 'baseExp':
|
||||||
default: return 0;
|
return sp.baseExp ?? 0;
|
||||||
|
case 'catchrate':
|
||||||
|
return sp.captureRate ?? 0;
|
||||||
|
case 'caught':
|
||||||
|
return gameEntry(sp.id).updatedAt || '';
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function metricLabel(key, sp) {
|
function metricLabel(key, sp) {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'bst': return `BST ${sp.bst}`;
|
case 'bst':
|
||||||
case 'hp': case 'atk': case 'def': case 'spa': case 'spd': case 'spe':
|
return `BST ${sp.bst}`;
|
||||||
|
case 'hp':
|
||||||
|
case 'atk':
|
||||||
|
case 'def':
|
||||||
|
case 'spa':
|
||||||
|
case 'spd':
|
||||||
|
case 'spe':
|
||||||
return `${STAT_SHORT[key]} ${sp.stats?.[key] ?? 0}`;
|
return `${STAT_SHORT[key]} ${sp.stats?.[key] ?? 0}`;
|
||||||
case 'height': return `${((sp.height ?? 0) / 10).toFixed(1)} m`;
|
case 'height':
|
||||||
case 'weight': return `${((sp.weight ?? 0) / 10).toFixed(1)} kg`;
|
return `${((sp.height ?? 0) / 10).toFixed(1)} m`;
|
||||||
case 'baseExp': return sp.baseExp != null ? `${sp.baseExp} EXP` : null;
|
case 'weight':
|
||||||
case 'catchrate': return `Catch ${sp.captureRate ?? '?'}`;
|
return `${((sp.weight ?? 0) / 10).toFixed(1)} kg`;
|
||||||
default: return null;
|
case 'baseExp':
|
||||||
|
return sp.baseExp != null ? `${sp.baseExp} EXP` : null;
|
||||||
|
case 'catchrate':
|
||||||
|
return `Catch ${sp.captureRate ?? '?'}`;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,7 +295,9 @@ export async function DexGrid() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
el('option', { value: '' }, 'Any egg group'),
|
el('option', { value: '' }, 'Any egg group'),
|
||||||
...eggGroupList.map((g) => el('option', { value: g, selected: g === filterEggGroup }, prettify(g))),
|
...eggGroupList.map((g) =>
|
||||||
|
el('option', { value: g, selected: g === filterEggGroup }, prettify(g)),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const minBstField = el(
|
const minBstField = el(
|
||||||
@ -347,7 +390,13 @@ export async function DexGrid() {
|
|||||||
'div',
|
'div',
|
||||||
{ class: `feed-tools${ui.get().toolsOpen ? ' is-open' : ''}` },
|
{ class: `feed-tools${ui.get().toolsOpen ? ' is-open' : ''}` },
|
||||||
filterBar,
|
filterBar,
|
||||||
el('div', { class: 'feed-sort' }, el('span', { class: 'feed-sort__label' }, 'Sort'), sortSelect, dirBtn),
|
el(
|
||||||
|
'div',
|
||||||
|
{ class: 'feed-sort' },
|
||||||
|
el('span', { class: 'feed-sort__label' }, 'Sort'),
|
||||||
|
sortSelect,
|
||||||
|
dirBtn,
|
||||||
|
),
|
||||||
el('div', { class: 'feed-sort feed-filters' }, typeSelect, genSelect, randomBtn),
|
el('div', { class: 'feed-sort feed-filters' }, typeSelect, genSelect, randomBtn),
|
||||||
el('div', { class: 'feed-sort feed-filters' }, abilitySelect, eggGroupSelect, minBstField),
|
el('div', { class: 'feed-sort feed-filters' }, abilitySelect, eggGroupSelect, minBstField),
|
||||||
el('div', { class: 'feed-sort feed-filters' }, fullyEvolvedCheck),
|
el('div', { class: 'feed-sort feed-filters' }, fullyEvolvedCheck),
|
||||||
@ -431,7 +480,9 @@ export async function DexGrid() {
|
|||||||
const gen = snap.versionGroupByKey.get(st.versionGroup)?.generation ?? 9;
|
const gen = snap.versionGroupByKey.get(st.versionGroup)?.generation ?? 9;
|
||||||
const boxed = st.spriteStyle === 'game' && gen <= 2;
|
const boxed = st.spriteStyle === 'game' && gen <= 2;
|
||||||
const filterTest = FILTERS.find((f) => f.key === filterKey).test;
|
const filterTest = FILTERS.find((f) => f.key === filterKey).test;
|
||||||
const abilityMon = filterAbility ? new Set(snap.abilityById.get(filterAbility)?.pokemon || []) : null;
|
const abilityMon = filterAbility
|
||||||
|
? new Set(snap.abilityById.get(filterAbility)?.pokemon || [])
|
||||||
|
: null;
|
||||||
|
|
||||||
let list = rows.filter(({ species, number }) => {
|
let list = rows.filter(({ species, number }) => {
|
||||||
if (
|
if (
|
||||||
@ -490,8 +541,7 @@ export async function DexGrid() {
|
|||||||
ids = rows.map((r) => r.species.id);
|
ids = rows.map((r) => r.species.id);
|
||||||
|
|
||||||
title.textContent = prettify(dex.name || dex.key);
|
title.textContent = prettify(dex.name || dex.key);
|
||||||
gameLabel.textContent =
|
gameLabel.textContent = st.versionGroup === 'all' ? 'All games' : prettify(st.versionGroup);
|
||||||
st.versionGroup === 'all' ? 'All games' : prettify(st.versionGroup);
|
|
||||||
refreshMeta();
|
refreshMeta();
|
||||||
paintGrid();
|
paintGrid();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -86,7 +86,11 @@ export async function ItemDetail(id) {
|
|||||||
}
|
}
|
||||||
if (shown.length > HOLDER_CAP) {
|
if (shown.length > HOLDER_CAP) {
|
||||||
holdersGrid.append(
|
holdersGrid.append(
|
||||||
el('p', { class: 'detail__muted' }, `+${shown.length - HOLDER_CAP} more — filter to narrow.`),
|
el(
|
||||||
|
'p',
|
||||||
|
{ class: 'detail__muted' },
|
||||||
|
`+${shown.length - HOLDER_CAP} more — filter to narrow.`,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -60,7 +60,8 @@ export async function MoveDetail(id) {
|
|||||||
`May ${prettify(d.meta.ailment.name)}${d.meta.ailment_chance ? ` (${d.meta.ailment_chance}%)` : ''}`,
|
`May ${prettify(d.meta.ailment.name)}${d.meta.ailment_chance ? ` (${d.meta.ailment_chance}%)` : ''}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (d.meta?.drain) bits.push(`${d.meta.drain > 0 ? 'Drains' : 'Recoil'} ${Math.abs(d.meta.drain)}%`);
|
if (d.meta?.drain)
|
||||||
|
bits.push(`${d.meta.drain > 0 ? 'Drains' : 'Recoil'} ${Math.abs(d.meta.drain)}%`);
|
||||||
if (d.meta?.healing) bits.push(`Heals ${d.meta.healing}%`);
|
if (d.meta?.healing) bits.push(`Heals ${d.meta.healing}%`);
|
||||||
if (d.meta?.crit_rate) bits.push(`+${d.meta.crit_rate} crit rate`);
|
if (d.meta?.crit_rate) bits.push(`+${d.meta.crit_rate} crit rate`);
|
||||||
if (d.meta?.flinch_chance) bits.push(`Flinch ${d.meta.flinch_chance}%`);
|
if (d.meta?.flinch_chance) bits.push(`Flinch ${d.meta.flinch_chance}%`);
|
||||||
@ -128,7 +129,11 @@ export async function MoveDetail(id) {
|
|||||||
}
|
}
|
||||||
if (shown.length > LEARNER_CAP) {
|
if (shown.length > LEARNER_CAP) {
|
||||||
learnersGrid.append(
|
learnersGrid.append(
|
||||||
el('p', { class: 'detail__muted' }, `+${shown.length - LEARNER_CAP} more — filter to narrow.`),
|
el(
|
||||||
|
'p',
|
||||||
|
{ class: 'detail__muted' },
|
||||||
|
`+${shown.length - LEARNER_CAP} more — filter to narrow.`,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,11 @@ export function NaturesView() {
|
|||||||
'header',
|
'header',
|
||||||
{ class: 'view__header' },
|
{ class: 'view__header' },
|
||||||
el('h1', {}, 'Natures'),
|
el('h1', {}, 'Natures'),
|
||||||
el('p', {}, 'Each nature raises one stat by 10% and lowers another by 10%. HP is never affected; five natures are neutral.'),
|
el(
|
||||||
|
'p',
|
||||||
|
{},
|
||||||
|
'Each nature raises one stat by 10% and lowers another by 10%. HP is never affected; five natures are neutral.',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
el(
|
el(
|
||||||
'div',
|
'div',
|
||||||
|
|||||||
@ -67,17 +67,13 @@ function typesForGeneration(pokemon, gen) {
|
|||||||
.sort((a, b) => a._gen - b._gen);
|
.sort((a, b) => a._gen - b._gen);
|
||||||
const era = past.find((p) => p._gen >= gen);
|
const era = past.find((p) => p._gen >= gen);
|
||||||
const list = era ? era.types : pokemon.types;
|
const list = era ? era.types : pokemon.types;
|
||||||
return list.slice().sort((a, b) => a.slot - b.slot).map((t) => t.type.name);
|
return list
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => a.slot - b.slot)
|
||||||
|
.map((t) => t.type.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
const STAT_NAMES = [
|
const STAT_NAMES = ['hp', 'attack', 'defense', 'special-attack', 'special-defense', 'speed'];
|
||||||
'hp',
|
|
||||||
'attack',
|
|
||||||
'defense',
|
|
||||||
'special-attack',
|
|
||||||
'special-defense',
|
|
||||||
'speed',
|
|
||||||
];
|
|
||||||
|
|
||||||
const NO_ENCOUNTER_DATA = new Set([
|
const NO_ENCOUNTER_DATA = new Set([
|
||||||
'scarlet-violet',
|
'scarlet-violet',
|
||||||
@ -106,10 +102,7 @@ export async function PokemonDetail(nationalId) {
|
|||||||
let pokemon;
|
let pokemon;
|
||||||
let species;
|
let species;
|
||||||
try {
|
try {
|
||||||
[pokemon, species] = await Promise.all([
|
[pokemon, species] = await Promise.all([getPokemon(nationalId), getSpecies(nationalId)]);
|
||||||
getPokemon(nationalId),
|
|
||||||
getSpecies(nationalId),
|
|
||||||
]);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
clear(view).append(
|
clear(view).append(
|
||||||
el(
|
el(
|
||||||
@ -272,7 +265,13 @@ export async function PokemonDetail(nationalId) {
|
|||||||
const on = inTeam(nationalId, teamForm());
|
const on = inTeam(nationalId, teamForm());
|
||||||
const full = team.get().members.length >= MAX_TEAM;
|
const full = team.get().members.length >= MAX_TEAM;
|
||||||
const label = activeForm ? `${activeForm.name} in team` : 'In team';
|
const label = activeForm ? `${activeForm.name} in team` : 'In team';
|
||||||
teamBtn.textContent = on ? `✓ ${label}` : full ? 'Team full' : activeForm ? `+ ${activeForm.name}` : '+ Team';
|
teamBtn.textContent = on
|
||||||
|
? `✓ ${label}`
|
||||||
|
: full
|
||||||
|
? 'Team full'
|
||||||
|
: activeForm
|
||||||
|
? `+ ${activeForm.name}`
|
||||||
|
: '+ Team';
|
||||||
teamBtn.classList.toggle('is-on', on);
|
teamBtn.classList.toggle('is-on', on);
|
||||||
teamBtn.disabled = !on && full;
|
teamBtn.disabled = !on && full;
|
||||||
}
|
}
|
||||||
@ -361,8 +360,10 @@ export async function PokemonDetail(nationalId) {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const evYield =
|
const evYield =
|
||||||
p.stats.filter((s) => s.effort > 0).map((s) => `${s.effort} ${STAT_LABEL[s.stat.name] || s.stat.name}`).join(', ') ||
|
p.stats
|
||||||
'—';
|
.filter((s) => s.effort > 0)
|
||||||
|
.map((s) => `${s.effort} ${STAT_LABEL[s.stat.name] || s.stat.name}`)
|
||||||
|
.join(', ') || '—';
|
||||||
const heldItems = p.held_items?.length
|
const heldItems = p.held_items?.length
|
||||||
? el(
|
? el(
|
||||||
'span',
|
'span',
|
||||||
@ -388,7 +389,10 @@ export async function PokemonDetail(nationalId) {
|
|||||||
abilityList.length && vgGen >= 3 ? fact('Abilities', abilitiesNode) : null,
|
abilityList.length && vgGen >= 3 ? fact('Abilities', abilitiesNode) : null,
|
||||||
fact('Introduced', prettify(species.generation.name)),
|
fact('Introduced', prettify(species.generation.name)),
|
||||||
species.genera?.length
|
species.genera?.length
|
||||||
? fact('Category', (species.genera.find((g) => g.language.name === 'en') || {}).genus || '—')
|
? fact(
|
||||||
|
'Category',
|
||||||
|
(species.genera.find((g) => g.language.name === 'en') || {}).genus || '—',
|
||||||
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
el('h3', { class: 'ppanel__sub' }, 'Training'),
|
el('h3', { class: 'ppanel__sub' }, 'Training'),
|
||||||
@ -466,7 +470,9 @@ export async function PokemonDetail(nationalId) {
|
|||||||
el('div', { class: 'fact' }, el('dt', {}, 'Gender'), genderCell(species.gender_rate)),
|
el('div', { class: 'fact' }, el('dt', {}, 'Gender'), genderCell(species.gender_rate)),
|
||||||
fact(
|
fact(
|
||||||
'Egg groups',
|
'Egg groups',
|
||||||
eggGroups.includes('Undiscovered') ? "Undiscovered (can't breed)" : eggGroups.join(', ') || '—',
|
eggGroups.includes('Undiscovered')
|
||||||
|
? "Undiscovered (can't breed)"
|
||||||
|
: eggGroups.join(', ') || '—',
|
||||||
),
|
),
|
||||||
fact(
|
fact(
|
||||||
'Egg cycles',
|
'Egg cycles',
|
||||||
@ -492,13 +498,19 @@ export async function PokemonDetail(nationalId) {
|
|||||||
);
|
);
|
||||||
} else if (NO_ENCOUNTER_DATA.has(st.versionGroup)) {
|
} else if (NO_ENCOUNTER_DATA.has(st.versionGroup)) {
|
||||||
locPanel.replaceChildren(
|
locPanel.replaceChildren(
|
||||||
el('p', { class: 'detail__muted' }, "PokéAPI doesn't have wild-encounter data for this game yet."),
|
el(
|
||||||
|
'p',
|
||||||
|
{ class: 'detail__muted' },
|
||||||
|
"PokéAPI doesn't have wild-encounter data for this game yet.",
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
getEncounters(nationalId)
|
getEncounters(nationalId)
|
||||||
.then((data) => locPanel.replaceChildren(Locations(data, vg ? vg.versions : [])))
|
.then((data) => locPanel.replaceChildren(Locations(data, vg ? vg.versions : [])))
|
||||||
.catch(() =>
|
.catch(() =>
|
||||||
locPanel.replaceChildren(el('p', { class: 'detail__muted' }, 'Location data unavailable offline.')),
|
locPanel.replaceChildren(
|
||||||
|
el('p', { class: 'detail__muted' }, 'Location data unavailable offline.'),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -516,7 +528,12 @@ export async function PokemonDetail(nationalId) {
|
|||||||
// generic "Default".
|
// generic "Default".
|
||||||
const DEFAULT_SLOT_NAME = { unown: 'A' };
|
const DEFAULT_SLOT_NAME = { unown: 'A' };
|
||||||
const slots = [
|
const slots = [
|
||||||
{ slug: snapSpecies.name, id: nationalId, name: DEFAULT_SLOT_NAME[snapSpecies.name] || 'Default', sprite: null },
|
{
|
||||||
|
slug: snapSpecies.name,
|
||||||
|
id: nationalId,
|
||||||
|
name: DEFAULT_SLOT_NAME[snapSpecies.name] || 'Default',
|
||||||
|
sprite: null,
|
||||||
|
},
|
||||||
...cosmeticForms,
|
...cosmeticForms,
|
||||||
];
|
];
|
||||||
const countEl = el('span', { class: 'formdex__count' });
|
const countEl = el('span', { class: 'formdex__count' });
|
||||||
@ -582,7 +599,9 @@ export async function PokemonDetail(nationalId) {
|
|||||||
// ---- Tabs -------------------------------------------
|
// ---- Tabs -------------------------------------------
|
||||||
const TABS = [
|
const TABS = [
|
||||||
{ id: 'about', label: 'About', node: aboutPanel },
|
{ id: 'about', label: 'About', node: aboutPanel },
|
||||||
formDexPanel ? { id: 'formdex', label: `${prettify(snapSpecies.name)} Dex`, node: formDexPanel } : null,
|
formDexPanel
|
||||||
|
? { id: 'formdex', label: `${prettify(snapSpecies.name)} Dex`, node: formDexPanel }
|
||||||
|
: null,
|
||||||
{ id: 'stats', label: 'Stats', node: statsPanel },
|
{ id: 'stats', label: 'Stats', node: statsPanel },
|
||||||
{ id: 'evo', label: 'Evolution', node: evoPanel },
|
{ id: 'evo', label: 'Evolution', node: evoPanel },
|
||||||
{ id: 'moves', label: 'Moves', node: movesPanel },
|
{ id: 'moves', label: 'Moves', node: movesPanel },
|
||||||
@ -745,7 +764,11 @@ export async function PokemonDetail(nationalId) {
|
|||||||
{ class: 'phero' },
|
{ class: 'phero' },
|
||||||
el('div', { class: 'phero__bg' }),
|
el('div', { class: 'phero__bg' }),
|
||||||
el('div', { class: 'phero__ball', 'aria-hidden': 'true' }),
|
el('div', { class: 'phero__ball', 'aria-hidden': 'true' }),
|
||||||
el('span', { class: 'phero__bignum', 'aria-hidden': 'true' }, `#${String(nationalId).padStart(4, '0')}`),
|
el(
|
||||||
|
'span',
|
||||||
|
{ class: 'phero__bignum', 'aria-hidden': 'true' },
|
||||||
|
`#${String(nationalId).padStart(4, '0')}`,
|
||||||
|
),
|
||||||
el(
|
el(
|
||||||
'div',
|
'div',
|
||||||
{ class: 'phero__top' },
|
{ class: 'phero__top' },
|
||||||
@ -753,23 +776,34 @@ export async function PokemonDetail(nationalId) {
|
|||||||
el('span', { class: 'phero__num' }, numLabel),
|
el('span', { class: 'phero__num' }, numLabel),
|
||||||
el('div', { class: 'phero__actions' }, cryBtn, shinyBtn, favBtn),
|
el('div', { class: 'phero__actions' }, cryBtn, shinyBtn, favBtn),
|
||||||
),
|
),
|
||||||
el(
|
el('div', { class: 'phero__head' }, heroName, heroTypes),
|
||||||
'div',
|
|
||||||
{ class: 'phero__head' },
|
|
||||||
heroName,
|
|
||||||
heroTypes,
|
|
||||||
),
|
|
||||||
formBar,
|
formBar,
|
||||||
cosmeticBar,
|
cosmeticBar,
|
||||||
el(
|
el(
|
||||||
'div',
|
'div',
|
||||||
{ class: 'phero__stage' },
|
{ class: 'phero__stage' },
|
||||||
prev
|
prev
|
||||||
? el('a', { class: 'phero__nav phero__nav--prev', href: `#/pokemon/${prev.species.id}`, title: prev.species.name }, '‹')
|
? el(
|
||||||
|
'a',
|
||||||
|
{
|
||||||
|
class: 'phero__nav phero__nav--prev',
|
||||||
|
href: `#/pokemon/${prev.species.id}`,
|
||||||
|
title: prev.species.name,
|
||||||
|
},
|
||||||
|
'‹',
|
||||||
|
)
|
||||||
: el('span', { class: 'phero__nav' }),
|
: el('span', { class: 'phero__nav' }),
|
||||||
artHolder,
|
artHolder,
|
||||||
next
|
next
|
||||||
? el('a', { class: 'phero__nav phero__nav--next', href: `#/pokemon/${next.species.id}`, title: next.species.name }, '›')
|
? el(
|
||||||
|
'a',
|
||||||
|
{
|
||||||
|
class: 'phero__nav phero__nav--next',
|
||||||
|
href: `#/pokemon/${next.species.id}`,
|
||||||
|
title: next.species.name,
|
||||||
|
},
|
||||||
|
'›',
|
||||||
|
)
|
||||||
: el('span', { class: 'phero__nav' }),
|
: el('span', { class: 'phero__nav' }),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -53,7 +53,9 @@ export async function ProgressView() {
|
|||||||
clear(list);
|
clear(list);
|
||||||
|
|
||||||
const gens = versionGroupsByGeneration(snap);
|
const gens = versionGroupsByGeneration(snap);
|
||||||
const anyPlayed = gens.some(({ versionGroups }) => versionGroups.some((vg) => isPlayed(vg.key)));
|
const anyPlayed = gens.some(({ versionGroups }) =>
|
||||||
|
versionGroups.some((vg) => isPlayed(vg.key)),
|
||||||
|
);
|
||||||
const showAll = ui.get().progShowAll || !anyPlayed;
|
const showAll = ui.get().progShowAll || !anyPlayed;
|
||||||
|
|
||||||
// National total — caught in any game.
|
// National total — caught in any game.
|
||||||
@ -115,7 +117,11 @@ export async function ProgressView() {
|
|||||||
render();
|
render();
|
||||||
|
|
||||||
clear(view).append(
|
clear(view).append(
|
||||||
el('nav', { class: 'lookup__nav' }, el('a', { class: 'link', href: '#/settings' }, '‹ Settings')),
|
el(
|
||||||
|
'nav',
|
||||||
|
{ class: 'lookup__nav' },
|
||||||
|
el('a', { class: 'link', href: '#/settings' }, '‹ Settings'),
|
||||||
|
),
|
||||||
el(
|
el(
|
||||||
'header',
|
'header',
|
||||||
{ class: 'view__header' },
|
{ class: 'view__header' },
|
||||||
|
|||||||
@ -9,11 +9,31 @@ import { TypeChip } from '../components/TypeChip.js';
|
|||||||
import { segThumb } from '../lib/anim.js';
|
import { segThumb } from '../lib/anim.js';
|
||||||
|
|
||||||
const idFromUrl = (u) => Number(u.replace(/\/$/, '').split('/').pop());
|
const idFromUrl = (u) => Number(u.replace(/\/$/, '').split('/').pop());
|
||||||
const loose = (s) => s.toLowerCase().replace(/[-\s]+/g, ' ').trim();
|
const loose = (s) =>
|
||||||
|
s
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[-\s]+/g, ' ')
|
||||||
|
.trim();
|
||||||
const DMG = { physical: 'Phys', special: 'Spec', status: 'Stat' };
|
const DMG = { physical: 'Phys', special: 'Spec', status: 'Stat' };
|
||||||
const TYPES = [
|
const TYPES = [
|
||||||
'normal', 'fire', 'water', 'electric', 'grass', 'ice', 'fighting', 'poison',
|
'normal',
|
||||||
'ground', 'flying', 'psychic', 'bug', 'rock', 'ghost', 'dragon', 'dark', 'steel', 'fairy',
|
'fire',
|
||||||
|
'water',
|
||||||
|
'electric',
|
||||||
|
'grass',
|
||||||
|
'ice',
|
||||||
|
'fighting',
|
||||||
|
'poison',
|
||||||
|
'ground',
|
||||||
|
'flying',
|
||||||
|
'psychic',
|
||||||
|
'bug',
|
||||||
|
'rock',
|
||||||
|
'ghost',
|
||||||
|
'dragon',
|
||||||
|
'dark',
|
||||||
|
'steel',
|
||||||
|
'fairy',
|
||||||
];
|
];
|
||||||
const itemSprite = (name) =>
|
const itemSprite = (name) =>
|
||||||
`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/${name}.png`;
|
`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/${name}.png`;
|
||||||
@ -30,7 +50,7 @@ const tmSortKey = (label) => {
|
|||||||
if (!label) return 1e9;
|
if (!label) return 1e9;
|
||||||
const m = String(label).match(/^([A-Z]+)0*(\d+)$/);
|
const m = String(label).match(/^([A-Z]+)0*(\d+)$/);
|
||||||
if (!m) return 1e9 - 1;
|
if (!m) return 1e9 - 1;
|
||||||
return (({ TM: 0, HM: 1, TR: 2 })[m[1]] ?? 3) * 1000 + Number(m[2]);
|
return ({ TM: 0, HM: 1, TR: 2 }[m[1]] ?? 3) * 1000 + Number(m[2]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function SearchView() {
|
export async function SearchView() {
|
||||||
@ -75,7 +95,14 @@ export async function SearchView() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const setThumb = segThumb(seg, TABS.length, Math.max(0, TABS.findIndex((t) => t.id === tab)));
|
const setThumb = segThumb(
|
||||||
|
seg,
|
||||||
|
TABS.length,
|
||||||
|
Math.max(
|
||||||
|
0,
|
||||||
|
TABS.findIndex((t) => t.id === tab),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const filters = el('div', { class: 'lookup__filters' });
|
const filters = el('div', { class: 'lookup__filters' });
|
||||||
const note = el('p', { class: 'search__note' });
|
const note = el('p', { class: 'search__note' });
|
||||||
@ -119,9 +146,7 @@ export async function SearchView() {
|
|||||||
const vg = settings.get().versionGroup;
|
const vg = settings.get().versionGroup;
|
||||||
const gameHasKind = (kind) =>
|
const gameHasKind = (kind) =>
|
||||||
snap.moves.some((m) =>
|
snap.moves.some((m) =>
|
||||||
m.machines.some(
|
m.machines.some((x) => (vg === 'all' || x.vg === vg) && (x.tm || '').startsWith(kind)),
|
||||||
(x) => (vg === 'all' || x.vg === vg) && (x.tm || '').startsWith(kind),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
const hasTm = gameHasKind('TM');
|
const hasTm = gameHasKind('TM');
|
||||||
const hasHm = gameHasKind('HM'); // false for Gen 8+ games, which dropped HMs
|
const hasHm = gameHasKind('HM'); // false for Gen 8+ games, which dropped HMs
|
||||||
@ -137,13 +162,23 @@ export async function SearchView() {
|
|||||||
|
|
||||||
filters.append(
|
filters.append(
|
||||||
...[
|
...[
|
||||||
pill('mvType', [['', 'Any type'], ...TYPES.map((t) => [t, prettify(t)])], ui.get().mvType, (v) => {
|
pill(
|
||||||
ui.set({ mvType: v });
|
'mvType',
|
||||||
run();
|
[['', 'Any type'], ...TYPES.map((t) => [t, prettify(t)])],
|
||||||
}),
|
ui.get().mvType,
|
||||||
|
(v) => {
|
||||||
|
ui.set({ mvType: v });
|
||||||
|
run();
|
||||||
|
},
|
||||||
|
),
|
||||||
pill(
|
pill(
|
||||||
'mvClass',
|
'mvClass',
|
||||||
[['', 'Any category'], ['physical', 'Physical'], ['special', 'Special'], ['status', 'Status']],
|
[
|
||||||
|
['', 'Any category'],
|
||||||
|
['physical', 'Physical'],
|
||||||
|
['special', 'Special'],
|
||||||
|
['status', 'Status'],
|
||||||
|
],
|
||||||
ui.get().mvClass,
|
ui.get().mvClass,
|
||||||
(v) => {
|
(v) => {
|
||||||
ui.set({ mvClass: v });
|
ui.set({ mvClass: v });
|
||||||
@ -181,16 +216,28 @@ export async function SearchView() {
|
|||||||
run();
|
run();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
pill('itSort', [['name', 'A–Z'], ['id', 'Dex order']], ui.get().itSort, (v) => {
|
pill(
|
||||||
ui.set({ itSort: v });
|
'itSort',
|
||||||
run();
|
[
|
||||||
}),
|
['name', 'A–Z'],
|
||||||
|
['id', 'Dex order'],
|
||||||
|
],
|
||||||
|
ui.get().itSort,
|
||||||
|
(v) => {
|
||||||
|
ui.set({ itSort: v });
|
||||||
|
run();
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else if (tab === 'abilities') {
|
} else if (tab === 'abilities') {
|
||||||
filters.append(
|
filters.append(
|
||||||
pill(
|
pill(
|
||||||
'abSort',
|
'abSort',
|
||||||
[['name', 'A–Z'], ['count', 'Most Pokémon'], ['gen', 'Newest']],
|
[
|
||||||
|
['name', 'A–Z'],
|
||||||
|
['count', 'Most Pokémon'],
|
||||||
|
['gen', 'Newest'],
|
||||||
|
],
|
||||||
ui.get().abSort,
|
ui.get().abSort,
|
||||||
(v) => {
|
(v) => {
|
||||||
ui.set({ abSort: v });
|
ui.set({ abSort: v });
|
||||||
@ -228,12 +275,11 @@ export async function SearchView() {
|
|||||||
const matches = snap.species
|
const matches = snap.species
|
||||||
.filter(
|
.filter(
|
||||||
(s) =>
|
(s) =>
|
||||||
loose(s.name).includes(q) ||
|
loose(s.name).includes(q) || String(s.id) === q || (s.types || []).some((t) => t === q),
|
||||||
String(s.id) === q ||
|
|
||||||
(s.types || []).some((t) => t === q),
|
|
||||||
)
|
)
|
||||||
.slice(0, 60);
|
.slice(0, 60);
|
||||||
if (!matches.length) return void results.append(el('p', { class: 'search__empty' }, 'No matches.'));
|
if (!matches.length)
|
||||||
|
return void results.append(el('p', { class: 'search__empty' }, 'No matches.'));
|
||||||
for (const s of matches) {
|
for (const s of matches) {
|
||||||
const e = entry(s.id);
|
const e = entry(s.id);
|
||||||
results.append(
|
results.append(
|
||||||
@ -289,8 +335,10 @@ export async function SearchView() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
list.sort((a, b) => {
|
list.sort((a, b) => {
|
||||||
if (sort === 'power') return (b.power ?? -1) - (a.power ?? -1) || a.name.localeCompare(b.name);
|
if (sort === 'power')
|
||||||
if (sort === 'accuracy') return (b.accuracy ?? -1) - (a.accuracy ?? -1) || a.name.localeCompare(b.name);
|
return (b.power ?? -1) - (a.power ?? -1) || a.name.localeCompare(b.name);
|
||||||
|
if (sort === 'accuracy')
|
||||||
|
return (b.accuracy ?? -1) - (a.accuracy ?? -1) || a.name.localeCompare(b.name);
|
||||||
if (sort === 'gen') return b.generation - a.generation || a.name.localeCompare(b.name);
|
if (sort === 'gen') return b.generation - a.generation || a.name.localeCompare(b.name);
|
||||||
if (sort === 'tm') {
|
if (sort === 'tm') {
|
||||||
return (
|
return (
|
||||||
@ -318,14 +366,24 @@ export async function SearchView() {
|
|||||||
'span',
|
'span',
|
||||||
{ class: 'search__types' },
|
{ class: 'search__types' },
|
||||||
TypeChip(m.type),
|
TypeChip(m.type),
|
||||||
el('span', { class: 'moverow__cat', dataset: { cat: m.damageClass || '' } }, DMG[m.damageClass] || '—'),
|
el(
|
||||||
|
'span',
|
||||||
|
{ class: 'moverow__cat', dataset: { cat: m.damageClass || '' } },
|
||||||
|
DMG[m.damageClass] || '—',
|
||||||
|
),
|
||||||
el('span', { class: 'search__num' }, m.power != null ? `${m.power} pw` : ''),
|
el('span', { class: 'search__num' }, m.power != null ? `${m.power} pw` : ''),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (list.length > shown.length) {
|
if (list.length > shown.length) {
|
||||||
results.append(el('p', { class: 'search__empty' }, `Showing ${shown.length} of ${list.length} — refine to see more.`));
|
results.append(
|
||||||
|
el(
|
||||||
|
'p',
|
||||||
|
{ class: 'search__empty' },
|
||||||
|
`Showing ${shown.length} of ${list.length} — refine to see more.`,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -343,7 +401,12 @@ export async function SearchView() {
|
|||||||
note.textContent = `${list.length} item${list.length === 1 ? '' : 's'}`;
|
note.textContent = `${list.length} item${list.length === 1 ? '' : 's'}`;
|
||||||
const shown = list.slice(0, 400);
|
const shown = list.slice(0, 400);
|
||||||
for (const it of shown) {
|
for (const it of shown) {
|
||||||
const icon = el('img', { class: 'search__item-icon', loading: 'lazy', alt: '', src: itemSprite(it.name) });
|
const icon = el('img', {
|
||||||
|
class: 'search__item-icon',
|
||||||
|
loading: 'lazy',
|
||||||
|
alt: '',
|
||||||
|
src: itemSprite(it.name),
|
||||||
|
});
|
||||||
icon.addEventListener('error', () => icon.remove(), { once: true });
|
icon.addEventListener('error', () => icon.remove(), { once: true });
|
||||||
results.append(
|
results.append(
|
||||||
el(
|
el(
|
||||||
@ -356,7 +419,13 @@ export async function SearchView() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (list.length > shown.length) {
|
if (list.length > shown.length) {
|
||||||
results.append(el('p', { class: 'search__empty' }, `Showing ${shown.length} of ${list.length} — refine to see more.`));
|
results.append(
|
||||||
|
el(
|
||||||
|
'p',
|
||||||
|
{ class: 'search__empty' },
|
||||||
|
`Showing ${shown.length} of ${list.length} — refine to see more.`,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -367,7 +436,8 @@ export async function SearchView() {
|
|||||||
(a) => !q || loose(a.name).includes(q) || a.effect.toLowerCase().includes(q),
|
(a) => !q || loose(a.name).includes(q) || a.effect.toLowerCase().includes(q),
|
||||||
);
|
);
|
||||||
list.sort((a, b) => {
|
list.sort((a, b) => {
|
||||||
if (sort === 'count') return b.pokemon.length - a.pokemon.length || a.name.localeCompare(b.name);
|
if (sort === 'count')
|
||||||
|
return b.pokemon.length - a.pokemon.length || a.name.localeCompare(b.name);
|
||||||
if (sort === 'gen') return b.generation - a.generation || a.name.localeCompare(b.name);
|
if (sort === 'gen') return b.generation - a.generation || a.name.localeCompare(b.name);
|
||||||
return a.name.localeCompare(b.name);
|
return a.name.localeCompare(b.name);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -14,16 +14,21 @@ export async function SettingsView() {
|
|||||||
const view = el('section', { class: 'view settings' });
|
const view = el('section', { class: 'view settings' });
|
||||||
const st = settings.get();
|
const st = settings.get();
|
||||||
|
|
||||||
const themeField = selectField('Theme', st.theme, [
|
const themeField = selectField(
|
||||||
['system', 'System'],
|
'Theme',
|
||||||
['light', 'Light'],
|
st.theme,
|
||||||
['dark', 'Dark'],
|
[
|
||||||
['black', 'Black (OLED)'],
|
['system', 'System'],
|
||||||
['sepia', 'Sepia'],
|
['light', 'Light'],
|
||||||
], (v) => {
|
['dark', 'Dark'],
|
||||||
settings.set({ theme: v });
|
['black', 'Black (OLED)'],
|
||||||
applyTheme();
|
['sepia', 'Sepia'],
|
||||||
});
|
],
|
||||||
|
(v) => {
|
||||||
|
settings.set({ theme: v });
|
||||||
|
applyTheme();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const ACCENTS = [
|
const ACCENTS = [
|
||||||
['red', '#b3161a'],
|
['red', '#b3161a'],
|
||||||
@ -55,16 +60,23 @@ export async function SettingsView() {
|
|||||||
);
|
);
|
||||||
const accentField = el('div', { class: 'field' }, el('span', {}, 'Accent color'), accentRow);
|
const accentField = el('div', { class: 'field' }, el('span', {}, 'Accent color'), accentRow);
|
||||||
|
|
||||||
const fontSizeField = selectField('Text size', st.fontScale || 'default', [
|
const fontSizeField = selectField(
|
||||||
['small', 'Small'],
|
'Text size',
|
||||||
['default', 'Default'],
|
st.fontScale || 'default',
|
||||||
['large', 'Large'],
|
[
|
||||||
], (v) => {
|
['small', 'Small'],
|
||||||
settings.set({ fontScale: v });
|
['default', 'Default'],
|
||||||
applyTheme();
|
['large', 'Large'],
|
||||||
});
|
],
|
||||||
|
(v) => {
|
||||||
|
settings.set({ fontScale: v });
|
||||||
|
applyTheme();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const reduceMotionField = el('label', { class: 'field field--check' },
|
const reduceMotionField = el(
|
||||||
|
'label',
|
||||||
|
{ class: 'field field--check' },
|
||||||
el('input', {
|
el('input', {
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: !!st.reduceMotion,
|
checked: !!st.reduceMotion,
|
||||||
@ -76,14 +88,21 @@ export async function SettingsView() {
|
|||||||
'Reduce motion',
|
'Reduce motion',
|
||||||
);
|
);
|
||||||
|
|
||||||
const spriteField = selectField('Sprite style', st.spriteStyle, [
|
const spriteField = selectField(
|
||||||
['default', 'Pixel (modern)'],
|
'Sprite style',
|
||||||
['game', 'Game era (pixel art from the selected game)'],
|
st.spriteStyle,
|
||||||
['official', 'Official artwork'],
|
[
|
||||||
['home', 'Pokémon HOME'],
|
['default', 'Pixel (modern)'],
|
||||||
], (v) => settings.set({ spriteStyle: v }));
|
['game', 'Game era (pixel art from the selected game)'],
|
||||||
|
['official', 'Official artwork'],
|
||||||
|
['home', 'Pokémon HOME'],
|
||||||
|
],
|
||||||
|
(v) => settings.set({ spriteStyle: v }),
|
||||||
|
);
|
||||||
|
|
||||||
const shinyField = el('label', { class: 'field field--check' },
|
const shinyField = el(
|
||||||
|
'label',
|
||||||
|
{ class: 'field field--check' },
|
||||||
el('input', {
|
el('input', {
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: st.showShiny,
|
checked: st.showShiny,
|
||||||
@ -92,7 +111,9 @@ export async function SettingsView() {
|
|||||||
'Default detail view to shiny sprites',
|
'Default detail view to shiny sprites',
|
||||||
);
|
);
|
||||||
|
|
||||||
const teamNavField = el('label', { class: 'field field--check' },
|
const teamNavField = el(
|
||||||
|
'label',
|
||||||
|
{ class: 'field field--check' },
|
||||||
el('input', {
|
el('input', {
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: st.showTeamNav !== false,
|
checked: st.showTeamNav !== false,
|
||||||
@ -101,7 +122,9 @@ export async function SettingsView() {
|
|||||||
'Show Team in the navigation bar',
|
'Show Team in the navigation bar',
|
||||||
);
|
);
|
||||||
|
|
||||||
const hapticsField = el('label', { class: 'field field--check' },
|
const hapticsField = el(
|
||||||
|
'label',
|
||||||
|
{ class: 'field field--check' },
|
||||||
el('input', {
|
el('input', {
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: st.haptics !== false,
|
checked: st.haptics !== false,
|
||||||
@ -110,11 +133,16 @@ export async function SettingsView() {
|
|||||||
'Haptic feedback on catch',
|
'Haptic feedback on catch',
|
||||||
);
|
);
|
||||||
|
|
||||||
const defaultRouteField = selectField('Landing screen', st.defaultRoute || 'dex', [
|
const defaultRouteField = selectField(
|
||||||
['dex', 'Dex'],
|
'Landing screen',
|
||||||
['team', 'Team'],
|
st.defaultRoute || 'dex',
|
||||||
['search', 'Search'],
|
[
|
||||||
], (v) => settings.set({ defaultRoute: v }));
|
['dex', 'Dex'],
|
||||||
|
['team', 'Team'],
|
||||||
|
['search', 'Search'],
|
||||||
|
],
|
||||||
|
(v) => settings.set({ defaultRoute: v }),
|
||||||
|
);
|
||||||
|
|
||||||
const storageNote = el('p', { class: 'settings__note' }, 'Calculating storage…');
|
const storageNote = el('p', { class: 'settings__note' }, 'Calculating storage…');
|
||||||
if (navigator.storage?.estimate) {
|
if (navigator.storage?.estimate) {
|
||||||
@ -157,7 +185,11 @@ export async function SettingsView() {
|
|||||||
body: `${dex.seen.length} seen · ${dex.caught.length} caught in this save.`,
|
body: `${dex.seen.length} seen · ${dex.caught.length} caught in this save.`,
|
||||||
choices: [
|
choices: [
|
||||||
{ key: 'merge', label: `Add to ${prettify(target)}` },
|
{ key: 'merge', label: `Add to ${prettify(target)}` },
|
||||||
{ key: 'replace', label: `Replace ${prettify(target)} with this`, class: 'button--danger' },
|
{
|
||||||
|
key: 'replace',
|
||||||
|
label: `Replace ${prettify(target)} with this`,
|
||||||
|
class: 'button--danger',
|
||||||
|
},
|
||||||
{ key: null, label: 'Cancel', class: 'button--ghost' },
|
{ key: null, label: 'Cancel', class: 'button--ghost' },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@ -253,50 +285,80 @@ export async function SettingsView() {
|
|||||||
el(
|
el(
|
||||||
'div',
|
'div',
|
||||||
{ class: 'settings__save' },
|
{ class: 'settings__save' },
|
||||||
el('button', { class: 'button', type: 'button', onclick: () => saveInput.click() }, 'Import from a game save'),
|
el(
|
||||||
|
'button',
|
||||||
|
{ class: 'button', type: 'button', onclick: () => saveInput.click() },
|
||||||
|
'Import from a game save',
|
||||||
|
),
|
||||||
el('p', { class: 'settings__note' }, 'A .sav / .srm / .dsv from a Gen 1–5 game.'),
|
el('p', { class: 'settings__note' }, 'A .sav / .srm / .dsv from a Gen 1–5 game.'),
|
||||||
),
|
),
|
||||||
saveInput,
|
saveInput,
|
||||||
saveNote,
|
saveNote,
|
||||||
storageNote,
|
storageNote,
|
||||||
el('div', { class: 'settings__actions' },
|
el(
|
||||||
el('button', {
|
'div',
|
||||||
class: 'button',
|
{ class: 'settings__actions' },
|
||||||
type: 'button',
|
el(
|
||||||
onclick: () => exportData(),
|
'button',
|
||||||
}, 'Export backup'),
|
{
|
||||||
el('button', {
|
class: 'button',
|
||||||
class: 'button',
|
type: 'button',
|
||||||
type: 'button',
|
onclick: () => exportData(),
|
||||||
onclick: () => importInput.click(),
|
},
|
||||||
}, 'Import backup'),
|
'Export backup',
|
||||||
|
),
|
||||||
|
el(
|
||||||
|
'button',
|
||||||
|
{
|
||||||
|
class: 'button',
|
||||||
|
type: 'button',
|
||||||
|
onclick: () => importInput.click(),
|
||||||
|
},
|
||||||
|
'Import backup',
|
||||||
|
),
|
||||||
importInput,
|
importInput,
|
||||||
el('button', {
|
el(
|
||||||
class: 'button button--danger',
|
'button',
|
||||||
type: 'button',
|
{
|
||||||
onclick: () => {
|
class: 'button button--danger',
|
||||||
if (confirm('Clear all seen/caught/favorite tracking? This cannot be undone.')) {
|
type: 'button',
|
||||||
selection.replace({ version: 3, pokemon: {}, games: {} });
|
onclick: () => {
|
||||||
}
|
if (confirm('Clear all seen/caught/favorite tracking? This cannot be undone.')) {
|
||||||
|
selection.replace({ version: 3, pokemon: {}, games: {} });
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}, 'Clear tracking data'),
|
'Clear tracking data',
|
||||||
el('button', {
|
),
|
||||||
class: 'button button--danger',
|
el(
|
||||||
type: 'button',
|
'button',
|
||||||
onclick: async () => {
|
{
|
||||||
if (!('caches' in window)) return;
|
class: 'button button--danger',
|
||||||
if (confirm('Clear cached PokéAPI data and sprites? They will re-download when online.')) {
|
type: 'button',
|
||||||
for (const key of await caches.keys()) await caches.delete(key);
|
onclick: async () => {
|
||||||
alert('Caches cleared.');
|
if (!('caches' in window)) return;
|
||||||
}
|
if (
|
||||||
|
confirm('Clear cached PokéAPI data and sprites? They will re-download when online.')
|
||||||
|
) {
|
||||||
|
for (const key of await caches.keys()) await caches.delete(key);
|
||||||
|
alert('Caches cleared.');
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}, 'Clear cached API data'),
|
'Clear cached API data',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
el('h2', {}, 'About'),
|
el('h2', {}, 'About'),
|
||||||
el('p', { class: 'settings__note' },
|
el(
|
||||||
|
'p',
|
||||||
|
{ class: 'settings__note' },
|
||||||
'Data from ',
|
'Data from ',
|
||||||
el('a', { href: 'https://pokeapi.co/', class: 'link', target: '_blank', rel: 'noreferrer' }, 'PokéAPI'),
|
el(
|
||||||
|
'a',
|
||||||
|
{ href: 'https://pokeapi.co/', class: 'link', target: '_blank', rel: 'noreferrer' },
|
||||||
|
'PokéAPI',
|
||||||
|
),
|
||||||
'. Tracking data and preferences are stored locally in your browser.',
|
'. Tracking data and preferences are stored locally in your browser.',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -311,9 +373,7 @@ function selectField(label, value, options, onChange) {
|
|||||||
const select = el(
|
const select = el(
|
||||||
'select',
|
'select',
|
||||||
{ onchange: (e) => onChange(e.target.value) },
|
{ onchange: (e) => onChange(e.target.value) },
|
||||||
...options.map(([v, text]) =>
|
...options.map(([v, text]) => el('option', { value: v, selected: v === value }, text)),
|
||||||
el('option', { value: v, selected: v === value }, text),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return el('label', { class: 'field' }, el('span', {}, label), select);
|
return el('label', { class: 'field' }, el('span', {}, label), select);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,13 +71,28 @@ export async function ShinyView() {
|
|||||||
'span',
|
'span',
|
||||||
{},
|
{},
|
||||||
el('span', { class: 'shiny__name' }, (sp?.name || `#${h.speciesId}`).replace(/-/g, ' ')),
|
el('span', { class: 'shiny__name' }, (sp?.name || `#${h.speciesId}`).replace(/-/g, ' ')),
|
||||||
el('span', { class: 'shiny__method' }, methodById(h.method).label + (h.charm ? ' · charm' : '')),
|
el(
|
||||||
|
'span',
|
||||||
|
{ class: 'shiny__method' },
|
||||||
|
methodById(h.method).label + (h.charm ? ' · charm' : ''),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
el(
|
el(
|
||||||
'div',
|
'div',
|
||||||
{ class: 'shiny__counter' },
|
{ class: 'shiny__counter' },
|
||||||
el('button', { type: 'button', class: 'shiny__step', onclick: () => { bumpHunt(h.id, -1); refresh(); } }, '−'),
|
el(
|
||||||
|
'button',
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
class: 'shiny__step',
|
||||||
|
onclick: () => {
|
||||||
|
bumpHunt(h.id, -1);
|
||||||
|
refresh();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'−',
|
||||||
|
),
|
||||||
count,
|
count,
|
||||||
el(
|
el(
|
||||||
'button',
|
'button',
|
||||||
@ -137,13 +152,25 @@ export async function ShinyView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function chooseMethod(done) {
|
function chooseMethod(done) {
|
||||||
const backdrop = el('div', { class: 'sheet-backdrop is-open', onclick: (e) => e.target === backdrop && backdrop.remove() });
|
const backdrop = el('div', {
|
||||||
const methodSel = el('select', { class: 'search__input' }, ...METHODS.map((m) => el('option', { value: m.id }, m.label)));
|
class: 'sheet-backdrop is-open',
|
||||||
|
onclick: (e) => e.target === backdrop && backdrop.remove(),
|
||||||
|
});
|
||||||
|
const methodSel = el(
|
||||||
|
'select',
|
||||||
|
{ class: 'search__input' },
|
||||||
|
...METHODS.map((m) => el('option', { value: m.id }, m.label)),
|
||||||
|
);
|
||||||
const charmChk = el('input', { type: 'checkbox' });
|
const charmChk = el('input', { type: 'checkbox' });
|
||||||
const panel = el(
|
const panel = el(
|
||||||
'div',
|
'div',
|
||||||
{ class: 'gsheet' },
|
{ class: 'gsheet' },
|
||||||
el('div', { class: 'gsheet__head' }, el('h2', {}, 'Hunt method'), el('button', { class: 'gsheet__close', onclick: () => backdrop.remove() }, '✕')),
|
el(
|
||||||
|
'div',
|
||||||
|
{ class: 'gsheet__head' },
|
||||||
|
el('h2', {}, 'Hunt method'),
|
||||||
|
el('button', { class: 'gsheet__close', onclick: () => backdrop.remove() }, '✕'),
|
||||||
|
),
|
||||||
methodSel,
|
methodSel,
|
||||||
el('label', { class: 'lookup__check', style: 'margin-top:12px' }, charmChk, 'Shiny Charm'),
|
el('label', { class: 'lookup__check', style: 'margin-top:12px' }, charmChk, 'Shiny Charm'),
|
||||||
el(
|
el(
|
||||||
@ -173,17 +200,30 @@ export async function ShinyView() {
|
|||||||
for (const h of hunts) list.append(huntCard(h));
|
for (const h of hunts) list.append(huntCard(h));
|
||||||
}
|
}
|
||||||
list.append(
|
list.append(
|
||||||
el('button', { type: 'button', class: 'lineup__add shiny__new', onclick: newHunt }, '+', el('span', {}, 'New hunt')),
|
el(
|
||||||
|
'button',
|
||||||
|
{ type: 'button', class: 'lineup__add shiny__new', onclick: newHunt },
|
||||||
|
'+',
|
||||||
|
el('span', {}, 'New hunt'),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
clear(view).append(
|
clear(view).append(
|
||||||
el('nav', { class: 'lookup__nav' }, el('a', { class: 'link', href: '#/settings' }, '‹ Settings')),
|
el(
|
||||||
|
'nav',
|
||||||
|
{ class: 'lookup__nav' },
|
||||||
|
el('a', { class: 'link', href: '#/settings' }, '‹ Settings'),
|
||||||
|
),
|
||||||
el(
|
el(
|
||||||
'header',
|
'header',
|
||||||
{ class: 'view__header' },
|
{ class: 'view__header' },
|
||||||
el('h1', {}, 'Shiny hunts'),
|
el('h1', {}, 'Shiny hunts'),
|
||||||
el('p', {}, 'Count encounters and watch the cumulative odds climb. “Found it” marks the Pokémon caught.'),
|
el(
|
||||||
|
'p',
|
||||||
|
{},
|
||||||
|
'Count encounters and watch the cumulative odds climb. “Found it” marks the Pokémon caught.',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
list,
|
list,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -16,12 +16,26 @@ import { openPokemonPicker } from '../components/PokemonPicker.js';
|
|||||||
import { segThumb } from '../lib/anim.js';
|
import { segThumb } from '../lib/anim.js';
|
||||||
|
|
||||||
const prettify = (s) => s.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
const prettify = (s) => s.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
||||||
const loose = (s) => s.toLowerCase().replace(/[-\s]+/g, ' ').trim();
|
const loose = (s) =>
|
||||||
|
s
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[-\s]+/g, ' ')
|
||||||
|
.trim();
|
||||||
const multClass = (m) =>
|
const multClass = (m) =>
|
||||||
m === 0 ? 'm0' : m === 0.25 ? 'm025' : m === 0.5 ? 'm05' : m === 2 ? 'm2' : m === 4 ? 'm4' : 'm1';
|
m === 0 ? 'm0' : m === 0.25 ? 'm025' : m === 0.5 ? 'm05' : m === 2 ? 'm2' : m === 4 ? 'm4' : 'm1';
|
||||||
const multText = (m) => (m === 0.25 ? '¼' : m === 0.5 ? '½' : m === 1 ? '' : `${m}×`);
|
const multText = (m) => (m === 0.25 ? '¼' : m === 0.5 ? '½' : m === 1 ? '' : `${m}×`);
|
||||||
const effWord = (eff) =>
|
const effWord = (eff) =>
|
||||||
eff === 0 ? 'No effect' : eff >= 4 ? '4× effective' : eff >= 2 ? 'Super effective' : eff <= 0.25 ? 'Barely effective' : eff <= 0.5 ? 'Not very effective' : null;
|
eff === 0
|
||||||
|
? 'No effect'
|
||||||
|
: eff >= 4
|
||||||
|
? '4× effective'
|
||||||
|
: eff >= 2
|
||||||
|
? 'Super effective'
|
||||||
|
: eff <= 0.25
|
||||||
|
? 'Barely effective'
|
||||||
|
: eff <= 0.5
|
||||||
|
? 'Not very effective'
|
||||||
|
: null;
|
||||||
|
|
||||||
export async function TeamView() {
|
export async function TeamView() {
|
||||||
const view = el('section', { class: 'view teamview' });
|
const view = el('section', { class: 'view teamview' });
|
||||||
@ -56,7 +70,14 @@ export async function TeamView() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const setThumb = segThumb(seg, MODES.length, Math.max(0, MODES.findIndex(([id]) => id === mode)));
|
const setThumb = segThumb(
|
||||||
|
seg,
|
||||||
|
MODES.length,
|
||||||
|
Math.max(
|
||||||
|
0,
|
||||||
|
MODES.findIndex(([id]) => id === mode),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const lineup = el('div', { class: 'lineup' });
|
const lineup = el('div', { class: 'lineup' });
|
||||||
const body = el('div', { class: 'teamview__body' });
|
const body = el('div', { class: 'teamview__body' });
|
||||||
@ -75,9 +96,24 @@ export async function TeamView() {
|
|||||||
el(
|
el(
|
||||||
'nav',
|
'nav',
|
||||||
{ class: 'teamview__refs', 'aria-label': 'Reference tools' },
|
{ class: 'teamview__refs', 'aria-label': 'Reference tools' },
|
||||||
el('a', { class: 'reflink', href: '#/natures' }, el('span', { class: 'reflink__ico', 'aria-hidden': 'true' }, '✦'), 'Natures'),
|
el(
|
||||||
el('a', { class: 'reflink', href: '#/types' }, el('span', { class: 'reflink__ico', 'aria-hidden': 'true' }, '▦'), 'Type chart'),
|
'a',
|
||||||
el('a', { class: 'reflink', href: '#/breeding' }, el('span', { class: 'reflink__ico', 'aria-hidden': 'true' }, '⬡'), 'Breeding'),
|
{ class: 'reflink', href: '#/natures' },
|
||||||
|
el('span', { class: 'reflink__ico', 'aria-hidden': 'true' }, '✦'),
|
||||||
|
'Natures',
|
||||||
|
),
|
||||||
|
el(
|
||||||
|
'a',
|
||||||
|
{ class: 'reflink', href: '#/types' },
|
||||||
|
el('span', { class: 'reflink__ico', 'aria-hidden': 'true' }, '▦'),
|
||||||
|
'Type chart',
|
||||||
|
),
|
||||||
|
el(
|
||||||
|
'a',
|
||||||
|
{ class: 'reflink', href: '#/breeding' },
|
||||||
|
el('span', { class: 'reflink__ico', 'aria-hidden': 'true' }, '⬡'),
|
||||||
|
'Breeding',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
lineup,
|
lineup,
|
||||||
@ -213,9 +249,7 @@ export async function TeamView() {
|
|||||||
const threats = analysis
|
const threats = analysis
|
||||||
.filter((a) => a.weakN >= 2 || (a.weakN >= 1 && a.resist === 0))
|
.filter((a) => a.weakN >= 2 || (a.weakN >= 1 && a.resist === 0))
|
||||||
.sort((a, b) => b.weakN - a.weakN || a.resist - b.resist);
|
.sort((a, b) => b.weakN - a.weakN || a.resist - b.resist);
|
||||||
const unresisted = analysis
|
const unresisted = analysis.filter((a) => a.weakN === 0 && a.resist === 0).map((a) => a.type);
|
||||||
.filter((a) => a.weakN === 0 && a.resist === 0)
|
|
||||||
.map((a) => a.type);
|
|
||||||
|
|
||||||
const weakSpots = threats.length
|
const weakSpots = threats.length
|
||||||
? el(
|
? el(
|
||||||
@ -282,7 +316,11 @@ export async function TeamView() {
|
|||||||
el(
|
el(
|
||||||
'span',
|
'span',
|
||||||
{ class: 'cov__bal-side cov__bal-side--r' },
|
{ class: 'cov__bal-side cov__bal-side--r' },
|
||||||
el('span', { class: 'cov__bal-weak', style: `width:${Math.min(a.weakN, 6) * 10}px` }, a.weakN || ''),
|
el(
|
||||||
|
'span',
|
||||||
|
{ class: 'cov__bal-weak', style: `width:${Math.min(a.weakN, 6) * 10}px` },
|
||||||
|
a.weakN || '',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return el(
|
return el(
|
||||||
@ -306,7 +344,9 @@ export async function TeamView() {
|
|||||||
.slice()
|
.slice()
|
||||||
.sort(
|
.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
(b.stats?.hp || 0) + (b.stats?.def || 0) + (b.stats?.spd || 0) -
|
(b.stats?.hp || 0) +
|
||||||
|
(b.stats?.def || 0) +
|
||||||
|
(b.stats?.spd || 0) -
|
||||||
((a.stats?.hp || 0) + (a.stats?.def || 0) + (a.stats?.spd || 0)),
|
((a.stats?.hp || 0) + (a.stats?.def || 0) + (a.stats?.spd || 0)),
|
||||||
)[0];
|
)[0];
|
||||||
|
|
||||||
@ -449,7 +489,11 @@ export async function TeamView() {
|
|||||||
Sprite(sp.spriteId ?? sp.id, { style, size: 48, alt: sp.name }),
|
Sprite(sp.spriteId ?? sp.id, { style, size: 48, alt: sp.name }),
|
||||||
el('span', {}, prettify(sp.name)),
|
el('span', {}, prettify(sp.name)),
|
||||||
)
|
)
|
||||||
: el('button', { type: 'button', class: 'calc__pick calc__pick--empty', onclick: () => pick() }, `Choose ${sideLabel}…`),
|
: el(
|
||||||
|
'button',
|
||||||
|
{ type: 'button', class: 'calc__pick calc__pick--empty', onclick: () => pick() },
|
||||||
|
`Choose ${sideLabel}…`,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
function pick() {
|
function pick() {
|
||||||
@ -482,7 +526,13 @@ export async function TeamView() {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
return el('div', { class: 'calc__side' }, el('h3', { class: 'ppanel__sub' }, sideLabel), holder, quick);
|
return el(
|
||||||
|
'div',
|
||||||
|
{ class: 'calc__side' },
|
||||||
|
el('h3', { class: 'ppanel__sub' }, sideLabel),
|
||||||
|
holder,
|
||||||
|
quick,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const atkSlot = slot('atk', 'Attacker');
|
const atkSlot = slot('atk', 'Attacker');
|
||||||
@ -550,12 +600,18 @@ export async function TeamView() {
|
|||||||
const v = forGeneration(raw, gen, genOfVg);
|
const v = forGeneration(raw, gen, genOfVg);
|
||||||
moveNote.textContent = `${prettify(v.type)} · ${v.damage_class ? prettify(v.damage_class) : 'Status'}${v.power ? ` · ${v.power} power` : ''}`;
|
moveNote.textContent = `${prettify(v.type)} · ${v.damage_class ? prettify(v.damage_class) : 'Status'}${v.power ? ` · ${v.power} power` : ''}`;
|
||||||
if (!atk || !def) {
|
if (!atk || !def) {
|
||||||
clear(result).append(el('p', { class: 'detail__muted' }, 'Choose an attacker and a defender.'));
|
clear(result).append(
|
||||||
|
el('p', { class: 'detail__muted' }, 'Choose an attacker and a defender.'),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!v.damage_class || v.damage_class === 'status' || !v.power) {
|
if (!v.damage_class || v.damage_class === 'status' || !v.power) {
|
||||||
clear(result).append(
|
clear(result).append(
|
||||||
el('p', { class: 'detail__muted' }, `${prettify(state.move.name)} doesn't deal direct damage.`),
|
el(
|
||||||
|
'p',
|
||||||
|
{ class: 'detail__muted' },
|
||||||
|
`${prettify(state.move.name)} doesn't deal direct damage.`,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -612,13 +668,23 @@ export async function TeamView() {
|
|||||||
el(
|
el(
|
||||||
'p',
|
'p',
|
||||||
{ class: 'calc__sub' },
|
{ class: 'calc__sub' },
|
||||||
hitsMin === hitsMax ? `${hitsMin} hit${hitsMin > 1 ? 's' : ''} to KO` : `${hitsMin}–${hitsMax} hits to KO`,
|
hitsMin === hitsMax
|
||||||
|
? `${hitsMin} hit${hitsMin > 1 ? 's' : ''} to KO`
|
||||||
|
: `${hitsMin}–${hitsMax} hits to KO`,
|
||||||
),
|
),
|
||||||
el(
|
el(
|
||||||
'div',
|
'div',
|
||||||
{ class: 'calc__badges' },
|
{ class: 'calc__badges' },
|
||||||
dmg.stab ? el('span', { class: 'calc__badge calc__badge--stab' }, 'STAB') : null,
|
dmg.stab ? el('span', { class: 'calc__badge calc__badge--stab' }, 'STAB') : null,
|
||||||
word ? el('span', { class: `calc__badge${dmg.eff >= 2 || dmg.eff === 4 ? ' calc__badge--up' : ' calc__badge--down'}` }, word) : null,
|
word
|
||||||
|
? el(
|
||||||
|
'span',
|
||||||
|
{
|
||||||
|
class: `calc__badge${dmg.eff >= 2 || dmg.eff === 4 ? ' calc__badge--up' : ' calc__badge--down'}`,
|
||||||
|
},
|
||||||
|
word,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,9 +7,24 @@ import { TypeChip } from '../components/TypeChip.js';
|
|||||||
import { typeHex } from '../lib/type-color.js';
|
import { typeHex } from '../lib/type-color.js';
|
||||||
|
|
||||||
const ABBR = {
|
const ABBR = {
|
||||||
normal: 'NOR', fire: 'FIR', water: 'WAT', electric: 'ELE', grass: 'GRA', ice: 'ICE',
|
normal: 'NOR',
|
||||||
fighting: 'FIG', poison: 'POI', ground: 'GRD', flying: 'FLY', psychic: 'PSY', bug: 'BUG',
|
fire: 'FIR',
|
||||||
rock: 'ROC', ghost: 'GHO', dragon: 'DRA', dark: 'DRK', steel: 'STE', fairy: 'FAI',
|
water: 'WAT',
|
||||||
|
electric: 'ELE',
|
||||||
|
grass: 'GRA',
|
||||||
|
ice: 'ICE',
|
||||||
|
fighting: 'FIG',
|
||||||
|
poison: 'POI',
|
||||||
|
ground: 'GRD',
|
||||||
|
flying: 'FLY',
|
||||||
|
psychic: 'PSY',
|
||||||
|
bug: 'BUG',
|
||||||
|
rock: 'ROC',
|
||||||
|
ghost: 'GHO',
|
||||||
|
dragon: 'DRA',
|
||||||
|
dark: 'DRK',
|
||||||
|
steel: 'STE',
|
||||||
|
fairy: 'FAI',
|
||||||
};
|
};
|
||||||
const cellText = (m) => (m === 0 ? '0' : m === 0.5 ? '½' : m === 2 ? '2' : '');
|
const cellText = (m) => (m === 0 ? '0' : m === 0.5 ? '½' : m === 2 ? '2' : '');
|
||||||
const cellClass = (m) => (m === 0 ? 'm0' : m === 0.5 ? 'm05' : m === 2 ? 'm2' : 'm1');
|
const cellClass = (m) => (m === 0 ? 'm0' : m === 0.5 ? 'm05' : m === 2 ? 'm2' : 'm1');
|
||||||
@ -78,11 +93,7 @@ export async function TypeChartView() {
|
|||||||
const m = multiplier(atk, [def], gen);
|
const m = multiplier(atk, [def], gen);
|
||||||
const hot = selected === atk || selected === def;
|
const hot = selected === atk || selected === def;
|
||||||
grid.append(
|
grid.append(
|
||||||
el(
|
el('span', { class: `tchart__cell ${cellClass(m)}${hot ? ' is-hot' : ''}` }, cellText(m)),
|
||||||
'span',
|
|
||||||
{ class: `tchart__cell ${cellClass(m)}${hot ? ' is-hot' : ''}` },
|
|
||||||
cellText(m),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -90,7 +101,11 @@ export async function TypeChartView() {
|
|||||||
clear(summary);
|
clear(summary);
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
summary.append(
|
summary.append(
|
||||||
el('p', { class: 'detail__muted' }, 'Tap a type on the edge to see everything it hits and everything that hits it.'),
|
el(
|
||||||
|
'p',
|
||||||
|
{ class: 'detail__muted' },
|
||||||
|
'Tap a type on the edge to see everything it hits and everything that hits it.',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -99,7 +114,12 @@ export async function TypeChartView() {
|
|||||||
const def = defensiveMatchups([selected], gen);
|
const def = defensiveMatchups([selected], gen);
|
||||||
const chipRow = (label, list) =>
|
const chipRow = (label, list) =>
|
||||||
list.length
|
list.length
|
||||||
? el('div', { class: 'tchart__srow' }, el('span', { class: 'tchart__slabel' }, label), el('span', { class: 'matchups__types' }, ...list.map(TypeChip)))
|
? el(
|
||||||
|
'div',
|
||||||
|
{ class: 'tchart__srow' },
|
||||||
|
el('span', { class: 'tchart__slabel' }, label),
|
||||||
|
el('span', { class: 'matchups__types' }, ...list.map(TypeChip)),
|
||||||
|
)
|
||||||
: null;
|
: null;
|
||||||
summary.append(
|
summary.append(
|
||||||
el('h3', { class: 'ppanel__sub' }, `${prettify(selected)} — attacking`),
|
el('h3', { class: 'ppanel__sub' }, `${prettify(selected)} — attacking`),
|
||||||
|
|||||||
@ -21,11 +21,7 @@ export default defineConfig({
|
|||||||
// The bundled PokéAPI snapshot can be a few hundred KB.
|
// The bundled PokéAPI snapshot can be a few hundred KB.
|
||||||
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
|
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
|
||||||
},
|
},
|
||||||
includeAssets: [
|
includeAssets: ['icon.svg', 'favicon.svg', 'apple-touch-icon.png'],
|
||||||
'icon.svg',
|
|
||||||
'favicon.svg',
|
|
||||||
'apple-touch-icon.png',
|
|
||||||
],
|
|
||||||
manifest: {
|
manifest: {
|
||||||
name: 'Pocketdex',
|
name: 'Pocketdex',
|
||||||
short_name: 'Pocketdex',
|
short_name: 'Pocketdex',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user