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
|
||||
about: Something isn't working right
|
||||
title: ""
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ""
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**What happened**
|
||||
<!-- and what you expected instead -->
|
||||
**What happened** (and what you expected instead)
|
||||
|
||||
**Steps to reproduce**
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
1. Go to …
|
||||
2. …
|
||||
3. …
|
||||
|
||||
**Context**
|
||||
|
||||
- Browser / OS:
|
||||
- Installed as a PWA or in a browser tab:
|
||||
- Selected game (if relevant):
|
||||
- Console errors (DevTools → Console):
|
||||
|
||||
**Screenshot / clip**
|
||||
<!-- optional but very helpful -->
|
||||
**Screenshot / clip** (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
|
||||
about: Suggest an idea
|
||||
title: ""
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ""
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**The idea**
|
||||
|
||||
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@ -1,18 +1,18 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: "/"
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: weekly
|
||||
groups:
|
||||
workbox:
|
||||
patterns:
|
||||
- "workbox-*"
|
||||
- 'workbox-*'
|
||||
dev-dependencies:
|
||||
dependency-type: development
|
||||
open-pull-requests-limit: 5
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
directory: "/"
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: monthly
|
||||
|
||||
@ -29,7 +29,7 @@ npm run preview # serve the production build — REQUIRED to test the se
|
||||
## Project structure
|
||||
|
||||
| Path | What |
|
||||
| --- | --- |
|
||||
| ---------------------------- | --------------------------------------------------------------------------- |
|
||||
| `src/main.js` | boot: theme, nav, router, SW registration, install prompt |
|
||||
| `src/router.js` | hash router; `routes` array; each view is `() => Promise<Node>` |
|
||||
| `src/views/*.js` | one file per route (`DexGrid`, `PokemonDetail`, `TeamView`, …) |
|
||||
@ -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
|
||||
animation.
|
||||
- 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
|
||||
runtime fetch.
|
||||
|
||||
|
||||
@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
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
|
||||
|
||||
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
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email address,
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email address,
|
||||
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
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
@ -65,7 +65,7 @@ code**:
|
||||
- Stores: `set(fn)` **replaces** state — spread `...s` yourself. Read with
|
||||
`get()`, react with `subscribe()`, and unsubscribe in the view's
|
||||
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.
|
||||
- Keep everything game-aware: if you touch typings, type effectiveness,
|
||||
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.
|
||||
- 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).
|
||||
- Fill in the PR template. Screenshots or a short clip for anything
|
||||
visual.
|
||||
|
||||
17
README.md
17
README.md
@ -39,7 +39,7 @@ npm run dev # http://localhost:5173
|
||||
```
|
||||
|
||||
| Script | What it does |
|
||||
| --- | --- |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `npm run snapshot` | Build `src/data/snapshot.json` from PokéAPI. Skips if the file is under 30 days old; `npm run snapshot -- --force` to rebuild anyway. |
|
||||
| `npm run dev` | Vite dev server with HMR. |
|
||||
| `npm run build` | Runs `snapshot` (as `prebuild`), then builds to `dist/`. |
|
||||
@ -54,6 +54,7 @@ the app has no data to show.
|
||||
## Features
|
||||
|
||||
### Dex grid
|
||||
|
||||
Type-tinted cards — official artwork over a type-coloured spotlight, ghost
|
||||
number, per-dex progress ring. Filter by name/number; chips for
|
||||
caught / missing / favourites / legendary / has-a-note; a filter drawer for
|
||||
@ -65,6 +66,7 @@ version-exclusive badge ("Ruby only"), derived from baked wild-encounter
|
||||
data (so gift/trade-only exclusives are missed).
|
||||
|
||||
### Detail page
|
||||
|
||||
Type-gradient hero with a shared-element morph from the tapped card, then a
|
||||
tabbed sheet:
|
||||
|
||||
@ -87,9 +89,10 @@ compact **Appearance** dropdown, plus a per-species "form dex" checklist
|
||||
tracked independently of the main caught flag.
|
||||
|
||||
### Team builder (`#/team`)
|
||||
|
||||
A lineup of up to 6 (specific forms included). Three modes:
|
||||
|
||||
- **Coverage** — leads with a *weak spots* summary (types that hit 2+
|
||||
- **Coverage** — leads with a _weak spots_ summary (types that hit 2+
|
||||
members, or that someone's weak to and nobody resists), then a full
|
||||
defensive matchup grid with a diverging resist ◀│▶ weak bar per type,
|
||||
offensive STAB gaps, and at-a-glance stats.
|
||||
@ -108,6 +111,7 @@ and a **Breeding** helper (`#/breeding` — egg-group browser, compatibility
|
||||
check, and egg moves resolved to the base breeding stage).
|
||||
|
||||
### Search (`#/search`)
|
||||
|
||||
Tabbed lookup — Pokémon, Moves, Items, Abilities — all browsable offline
|
||||
from the snapshot. Moves filter by type / damage class / "TMs only" /
|
||||
"HMs only" for the selected game and sort by power / accuracy / recency, or
|
||||
@ -117,23 +121,26 @@ move, ability and item detail pages each list the Pokémon connected to them
|
||||
selected game. Query, scroll, tab and filters persist.
|
||||
|
||||
### Progress by game (`#/progress`)
|
||||
|
||||
Every game's seen/caught tally against its own dex, from its own per-game
|
||||
bucket. Leads with the games you've played (selected at least once, or
|
||||
imported a save for); the rest sit behind a toggle. The National Dex row is
|
||||
the union across all games.
|
||||
|
||||
### Shiny hunt tracker (`#/shiny`)
|
||||
|
||||
Per-hunt counters with method-aware odds (full odds, Masuda, SOS, chain
|
||||
fishing, DexNav, radar, dynamax adventures, outbreaks, mass outbreaks) and
|
||||
Shiny Charm, cumulative-probability readout, notes, and a "found it" that
|
||||
marks the Pokémon caught.
|
||||
|
||||
### Save-file import
|
||||
|
||||
**Settings → Import from a game save.** Reads the game's own seen/owned
|
||||
bitfields — not an approximation — from:
|
||||
|
||||
| Gen | Games | Format |
|
||||
| --- | --- | --- |
|
||||
| --- | -------------------------------------------------- | -------------------------------------------------------- |
|
||||
| 1 | Red / Blue / Yellow | 32 KB SRAM (`.sav` / `.srm`) |
|
||||
| 2 | Gold / Silver / Crystal | 32 KB SRAM — checksum picks G/S vs Crystal |
|
||||
| 3 | Ruby / Sapphire / Emerald, FireRed / LeafGreen | 128 KB — newer slot, rotated sections |
|
||||
@ -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.
|
||||
|
||||
### Settings
|
||||
|
||||
Theme (System / Light / Dark / Black OLED / Sepia) + accent colour; text
|
||||
size; an in-app reduce-motion override (the OS preference is always
|
||||
respected too); sprite style (modern pixel / game-era pixel / official
|
||||
@ -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.
|
||||
|
||||
### PWA
|
||||
|
||||
Workbox service worker: precache the app shell + snapshot; stale-while-
|
||||
revalidate for PokéAPI JSON (30-day TTL); cache-first LRU for sprites;
|
||||
in-app update toast. Installable with maskable icons, `beforeinstallprompt`
|
||||
@ -170,7 +179,7 @@ Type chart.
|
||||
### Data & storage
|
||||
|
||||
| Layer | Where | Notes |
|
||||
| --- | --- | --- |
|
||||
| ---------------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Preferences + tracking | `localStorage` (`pdx.*` keys) | Tiny, synchronous, restored on reload. |
|
||||
| Build-time snapshot | `src/data/snapshot.json` (~150 KB) | Every species (id, name, generation, current + past typings, base stats, height/weight, species flags), every regional Pokédex, every version group, plus move / item / ability name indexes and baked TM numbers and version-exclusivity. Precached by the SW. |
|
||||
| Detail data | `pokeapi.co`, fetched lazily per page | Stats, abilities, flavour text, evolution, encounters. Cached by the SW (stale-while-revalidate). |
|
||||
|
||||
@ -10,8 +10,8 @@ branches.
|
||||
|
||||
Please **don't** open a public issue for a security problem.
|
||||
|
||||
Use GitHub's **private vulnerability reporting** (the *Report a
|
||||
vulnerability* button under the repository's *Security* tab). Include what
|
||||
Use GitHub's **private vulnerability reporting** (the _Report a
|
||||
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
|
||||
an acknowledgement as soon as possible.
|
||||
|
||||
|
||||
@ -6,5 +6,5 @@ services:
|
||||
BASE_PATH: /
|
||||
image: pocketdex
|
||||
ports:
|
||||
- "8080:80"
|
||||
- '8080:80'
|
||||
restart: unless-stopped
|
||||
|
||||
@ -2,10 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, viewport-fit=cover"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#b3161a" />
|
||||
<meta
|
||||
name="description"
|
||||
|
||||
@ -111,9 +111,7 @@ async function isFresh() {
|
||||
|
||||
async function main() {
|
||||
if (!FORCE && (await isFresh())) {
|
||||
console.log(
|
||||
`snapshot.json is fresh (< ${MAX_AGE_DAYS} days old). Use --force to rebuild.`,
|
||||
);
|
||||
console.log(`snapshot.json is fresh (< ${MAX_AGE_DAYS} days old). Use --force to rebuild.`);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -170,15 +168,11 @@ async function main() {
|
||||
pokedexKeys: data.pokedexes.map((x) => x.name),
|
||||
});
|
||||
});
|
||||
versionGroups.sort(
|
||||
(a, b) => a.generation - b.generation || a.key.localeCompare(b.key),
|
||||
);
|
||||
versionGroups.sort((a, b) => a.generation - b.generation || a.key.localeCompare(b.key));
|
||||
|
||||
// ---- Species: the data-rich part -----------------------------
|
||||
const speciesIndex = await api('pokemon-species?limit=100000');
|
||||
const ids = speciesIndex.results
|
||||
.map((r) => idFromUrl(r.url))
|
||||
.sort((a, b) => a - b);
|
||||
const ids = speciesIndex.results.map((r) => idFromUrl(r.url)).sort((a, b) => a - b);
|
||||
|
||||
const statMap = (pk) => {
|
||||
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 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}`;
|
||||
|
||||
console.log(`Fetching ${ids.length} species (stats, typings, flags, forms) …`);
|
||||
const species = await mapLimit(ids, CONCURRENCY, async (id) => {
|
||||
const [pk, sp] = await Promise.all([
|
||||
api(`pokemon/${id}`),
|
||||
api(`pokemon-species/${id}`),
|
||||
]);
|
||||
const [pk, sp] = await Promise.all([api(`pokemon/${id}`), api(`pokemon-species/${id}`)]);
|
||||
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 baseStatKey = statKey(stats);
|
||||
|
||||
@ -226,7 +224,10 @@ async function main() {
|
||||
continue;
|
||||
}
|
||||
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 entry = {
|
||||
slug,
|
||||
@ -296,7 +297,10 @@ async function main() {
|
||||
.map((t) => t.type.name),
|
||||
pastTypes: (pk.past_types || []).map((p) => ({
|
||||
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,
|
||||
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.
|
||||
const versionToVg = {};
|
||||
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 …`);
|
||||
await mapLimit(species, CONCURRENCY, async (sp) => {
|
||||
let enc;
|
||||
@ -410,10 +416,7 @@ async function main() {
|
||||
name: a.name,
|
||||
generation: idFromUrl(a.generation.url),
|
||||
isMainSeries: a.is_main_series,
|
||||
effect:
|
||||
(en && (en.short_effect || en.effect)) ||
|
||||
(flavour && flavour.flavor_text) ||
|
||||
'',
|
||||
effect: (en && (en.short_effect || en.effect)) || (flavour && flavour.flavor_text) || '',
|
||||
pokemon: [
|
||||
...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
|
||||
* 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 types = typesForGen(species, gen);
|
||||
const mainType = types[0] || 'normal';
|
||||
@ -61,7 +65,8 @@ export function Card(species, number, { spriteStyle = 'official', versionGroup,
|
||||
onclick: () => {
|
||||
// Tag this sprite so the router's view transition morphs it into
|
||||
// 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');
|
||||
},
|
||||
},
|
||||
@ -85,7 +90,11 @@ export function Card(species, number, { spriteStyle = 'official', versionGroup,
|
||||
(() => {
|
||||
const ex = versionGroup && versionGroup !== 'all' && species.exclusiveIn?.[versionGroup];
|
||||
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;
|
||||
})(),
|
||||
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
|
||||
* 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();
|
||||
let pruned = false;
|
||||
|
||||
@ -95,9 +98,7 @@ export function EvolutionChain(chain, { style = 'default', versionGroup, genOf =
|
||||
roots.forEach((r) => renderNode(r, 0));
|
||||
|
||||
if (pruned) {
|
||||
wrap.append(
|
||||
el('p', { class: 'evo__note' }, 'Cross-generation stages hidden for this game.'),
|
||||
);
|
||||
wrap.append(el('p', { class: 'evo__note' }, 'Cross-generation stages hidden for this game.'));
|
||||
}
|
||||
return wrap;
|
||||
}
|
||||
|
||||
@ -96,9 +96,7 @@ export async function openGameSheet() {
|
||||
);
|
||||
|
||||
for (const { generation, versionGroups } of versionGroupsByGeneration(snap)) {
|
||||
list.append(
|
||||
el('h3', { class: 'gsheet__gen' }, `Gen ${generation.id} · ${generation.name}`),
|
||||
);
|
||||
list.append(el('h3', { class: 'gsheet__gen' }, `Gen ${generation.id} · ${generation.name}`));
|
||||
const grid = el('div', { class: 'gsheet__grid' });
|
||||
for (const vg of versionGroups) {
|
||||
const active = vg.key === st.versionGroup;
|
||||
@ -126,7 +124,11 @@ export async function openGameSheet() {
|
||||
'span',
|
||||
{ class: 'gamecard__foot' },
|
||||
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) {
|
||||
const list = moveData.machines || [];
|
||||
if (!list.length) return null;
|
||||
const hit =
|
||||
list.find((x) => x.version_group.name === versionGroupKey) || list[list.length - 1];
|
||||
const hit = list.find((x) => x.version_group.name === versionGroupKey) || list[list.length - 1];
|
||||
try {
|
||||
const mc = await getMachine(Number(hit.machine.url.replace(/\/$/, '').split('/').pop()));
|
||||
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,
|
||||
// not set per move.
|
||||
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) {
|
||||
@ -145,7 +152,11 @@ export function MovesList(pokemonMoves, { versionGroupKey, gen = 9, genOfVg = ()
|
||||
const enrichers = [];
|
||||
|
||||
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 tmCell = method === 'machine' ? el('span', { class: 'moverow__tm' }) : null;
|
||||
let bodyFilled = false;
|
||||
@ -179,7 +190,9 @@ export function MovesList(pokemonMoves, { versionGroupKey, gen = 9, genOfVg = ()
|
||||
fillBody(body, m.data, v);
|
||||
await fillTm();
|
||||
} 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.target?.name) bits.push(`Target: ${d.target.name.replace(/-/g, ' ')}`);
|
||||
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?.crit_rate) bits.push(`+${d.meta.crit_rate} crit rate`);
|
||||
if (d.meta?.flinch_chance) bits.push(`Flinch ${d.meta.flinch_chance}%`);
|
||||
|
||||
@ -4,7 +4,13 @@ import { settings } from '../store/settings.js';
|
||||
|
||||
const ITEMS = [
|
||||
{ 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: 'Search', icon: '⌕', href: '#/search', match: (h) => h.startsWith('#/search') },
|
||||
{ label: 'Settings', icon: '⚙', href: '#/settings', match: (h) => h.startsWith('#/settings') },
|
||||
@ -28,7 +34,11 @@ export function Nav() {
|
||||
el('span', { class: 'nav__label' }, item.label),
|
||||
];
|
||||
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);
|
||||
});
|
||||
nav.replaceChildren(el('span', { class: 'nav__brand' }, 'Pocketdex'), ...links);
|
||||
|
||||
@ -88,7 +88,11 @@ export async function openPokemonPicker(onPick, { closeAfterPick = false } = {})
|
||||
'div',
|
||||
{ class: 'gsheet__head' },
|
||||
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);
|
||||
panel.append(head, input, results);
|
||||
|
||||
@ -40,7 +40,12 @@ export function ProgressRing() {
|
||||
const value = el('span', { class: 'ring__value' }, num, tail);
|
||||
const node = el(
|
||||
'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,
|
||||
el('div', { class: 'ring__label' }, value),
|
||||
);
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { el } from '../lib/dom.js';
|
||||
import { gameSpriteUrl } from '../data/game-sprites.js';
|
||||
|
||||
const SPRITES =
|
||||
'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon';
|
||||
const SPRITES = 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon';
|
||||
|
||||
function pixel(id, shiny) {
|
||||
return `${SPRITES}${shiny ? '/shiny' : ''}/${id}.png`;
|
||||
|
||||
@ -2,8 +2,7 @@ import { el } from '../lib/dom.js';
|
||||
import { Sprite } from './Sprite.js';
|
||||
import { settings } from '../store/settings.js';
|
||||
|
||||
const bar = (w, h = 14) =>
|
||||
el('span', { class: 'sk-bar', style: `width:${w};height:${h}px` });
|
||||
const bar = (w, h = 14) => el('span', { class: 'sk-bar', style: `width:${w};height:${h}px` });
|
||||
|
||||
const facts = () =>
|
||||
el(
|
||||
@ -43,11 +42,7 @@ export function detailSkeleton(match) {
|
||||
el(
|
||||
'div',
|
||||
{ class: 'psheet' },
|
||||
el(
|
||||
'div',
|
||||
{ class: 'psheet__tabs' },
|
||||
...Array.from({ length: 5 }, () => bar('58px', 18)),
|
||||
),
|
||||
el('div', { class: 'psheet__tabs' }, ...Array.from({ length: 5 }, () => bar('58px', 18))),
|
||||
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
|
||||
* pixel sprites — those return null and the caller falls back to HOME art.
|
||||
*/
|
||||
const REPO =
|
||||
'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions';
|
||||
const REPO = 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions';
|
||||
|
||||
const PATH_BY_VERSION_GROUP = {
|
||||
'red-blue': 'generation-i/red-blue',
|
||||
@ -26,8 +25,7 @@ const PATH_BY_VERSION_GROUP = {
|
||||
'omega-ruby-alpha-sapphire': 'generation-vi/omegaruby-alphasapphire',
|
||||
'sun-moon': 'generation-vii/ultra-sun-ultra-moon',
|
||||
'ultra-sun-ultra-moon': 'generation-vii/ultra-sun-ultra-moon',
|
||||
'brilliant-diamond-and-shining-pearl':
|
||||
'generation-viii/brilliant-diamond-shining-pearl',
|
||||
'brilliant-diamond-and-shining-pearl': 'generation-viii/brilliant-diamond-shining-pearl',
|
||||
};
|
||||
|
||||
/** URL for a game-era pixel sprite, or null if that game has none. */
|
||||
|
||||
@ -4,18 +4,14 @@
|
||||
*/
|
||||
|
||||
export function prettify(key) {
|
||||
return key
|
||||
.replace(/-/g, ' ')
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
return key.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
/** Regional dexes belonging to a version group, in the API's order. */
|
||||
export function dexesForVersionGroup(snap, versionGroupKey) {
|
||||
const vg = snap.versionGroupByKey.get(versionGroupKey);
|
||||
const keys = vg && vg.pokedexKeys.length ? vg.pokedexKeys : ['national'];
|
||||
return keys
|
||||
.map((k) => snap.pokedexByKey.get(k))
|
||||
.filter(Boolean);
|
||||
return keys.map((k) => snap.pokedexByKey.get(k)).filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -5,9 +5,24 @@
|
||||
* CHART[attacking][defending] holds only the non-1x multipliers.
|
||||
*/
|
||||
export const TYPES = [
|
||||
'normal', 'fire', 'water', 'electric', 'grass', 'ice', 'fighting', 'poison',
|
||||
'ground', 'flying', 'psychic', 'bug', 'rock', 'ghost', 'dragon', 'dark',
|
||||
'steel', 'fairy',
|
||||
'normal',
|
||||
'fire',
|
||||
'water',
|
||||
'electric',
|
||||
'grass',
|
||||
'ice',
|
||||
'fighting',
|
||||
'poison',
|
||||
'ground',
|
||||
'flying',
|
||||
'psychic',
|
||||
'bug',
|
||||
'rock',
|
||||
'ghost',
|
||||
'dragon',
|
||||
'dark',
|
||||
'steel',
|
||||
'fairy',
|
||||
];
|
||||
|
||||
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 },
|
||||
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 },
|
||||
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 },
|
||||
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 },
|
||||
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 },
|
||||
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 },
|
||||
ghost: { normal: 0, psychic: 2, ghost: 2, dark: 0.5 },
|
||||
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': [...] }
|
||||
*/
|
||||
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,
|
||||
// Fairy before Gen 6.
|
||||
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
|
||||
* 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}`;
|
||||
if (prefersReducedMotion() || duration <= 0 || from === 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.
|
||||
*/
|
||||
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;
|
||||
const base = Math.floor((Math.floor((2 * level) / 5 + 2) * power * (atkStat / defStat)) / 50) + 2;
|
||||
const stab = atkTypes.includes(moveType) ? 1.5 : 1;
|
||||
|
||||
@ -32,7 +32,16 @@ const GB_CHARS = (() => {
|
||||
t[0xa0 + i] = lo[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;
|
||||
})();
|
||||
|
||||
@ -47,7 +56,11 @@ const GBA_CHARS = (() => {
|
||||
t[0xd5 + i] = lo[i];
|
||||
}
|
||||
Object.assign(t, {
|
||||
0xad: '.', 0xae: '-', 0xba: '/', 0xac: ',', 0xb8: '&',
|
||||
0xad: '.',
|
||||
0xae: '-',
|
||||
0xba: '/',
|
||||
0xac: ',',
|
||||
0xb8: '&',
|
||||
});
|
||||
return t;
|
||||
})();
|
||||
@ -76,13 +89,21 @@ const sumBytes = (bytes, from, to) => {
|
||||
* 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) {
|
||||
if (bytes.length < 0x8000) return null;
|
||||
const caught = bitsToList(bytes, G1.owned, 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;
|
||||
return {
|
||||
gen: 1,
|
||||
@ -101,8 +122,26 @@ function tryGen1(bytes) {
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
const G2 = {
|
||||
gs: { 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'] },
|
||||
gs: {
|
||||
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;
|
||||
|
||||
@ -145,7 +184,9 @@ const SECTION_SIZE = 4096;
|
||||
const SECTIONS_PER_SLOT = 14;
|
||||
const SLOT_SIZE = SECTION_SIZE * SECTIONS_PER_SLOT; // 57344
|
||||
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_MAGIC = DEX_OFFSET + 2; // 0xDA = RSE national, 0xB9 = FRLG national
|
||||
const G3_OWNED = DEX_OFFSET + 16;
|
||||
@ -179,7 +220,10 @@ function tryGen3(bytes) {
|
||||
if (bytes.length < SLOT_SIZE + SECTION_SIZE) return null;
|
||||
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
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;
|
||||
const slot = b.counter > a.counter ? b : a;
|
||||
const s0 = slot.sections[0];
|
||||
@ -222,8 +266,15 @@ function ndsNormalise(bytes) {
|
||||
let hasMarker = false;
|
||||
for (let i = 0; i <= tail.length - marker.length; i++) {
|
||||
let ok = true;
|
||||
for (let j = 0; j < marker.length; j++) if (tail[i + j] !== marker.charCodeAt(j)) { ok = false; break; }
|
||||
if (ok) { hasMarker = true; break; }
|
||||
for (let j = 0; j < marker.length; j++)
|
||||
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);
|
||||
}
|
||||
@ -298,7 +349,12 @@ function readGen5Slot(bytes, slotBase, v) {
|
||||
const caught = bitsToList(bytes, p + GEN5.caught, GEN5.species);
|
||||
const seen = orSeen(bytes, p, GEN5.seen, GEN5.size, GEN5.species);
|
||||
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) {
|
||||
|
||||
@ -20,7 +20,11 @@ export function onSwipe(target, { onLeft, onRight, threshold = 60 } = {}) {
|
||||
// the page? (moves list, tab strip, coverage grid…) `dir` < 0 means the
|
||||
// finger is moving left — that scrolls such an element to the right.
|
||||
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) {
|
||||
const ox = getComputedStyle(n).overflowX;
|
||||
if (ox === 'auto' || ox === 'scroll') {
|
||||
|
||||
@ -2,9 +2,24 @@
|
||||
// custom properties once and memoising. Type colors do not change between
|
||||
// light and dark, so the cache is safe for the session.
|
||||
const TYPES = [
|
||||
'normal', 'fire', 'water', 'electric', 'grass', 'ice', 'fighting', 'poison',
|
||||
'ground', 'flying', 'psychic', 'bug', 'rock', 'ghost', 'dragon', 'dark',
|
||||
'steel', 'fairy',
|
||||
'normal',
|
||||
'fire',
|
||||
'water',
|
||||
'electric',
|
||||
'grass',
|
||||
'ice',
|
||||
'fighting',
|
||||
'poison',
|
||||
'ground',
|
||||
'flying',
|
||||
'psychic',
|
||||
'bug',
|
||||
'rock',
|
||||
'ghost',
|
||||
'dragon',
|
||||
'dark',
|
||||
'steel',
|
||||
'fairy',
|
||||
];
|
||||
|
||||
let cache;
|
||||
|
||||
@ -41,9 +41,7 @@ settings.subscribe((s) => {
|
||||
if ((location.hash || '').startsWith('#/pokemon/')) rerender();
|
||||
}
|
||||
});
|
||||
window
|
||||
.matchMedia('(prefers-color-scheme: dark)')
|
||||
.addEventListener('change', () => applyTheme());
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => applyTheme());
|
||||
|
||||
const app = document.getElementById('app');
|
||||
app.replaceChildren();
|
||||
|
||||
@ -18,10 +18,18 @@ import { prefersReducedMotion } from './store/settings.js';
|
||||
|
||||
const routes = [
|
||||
{ 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: /^#\/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: /^#\/natures$/, view: () => NaturesView() },
|
||||
{ pattern: /^#\/types$/, view: () => TypeChartView() },
|
||||
|
||||
@ -34,10 +34,7 @@ export function createStore(key, initial) {
|
||||
return {
|
||||
get: () => state,
|
||||
set(patch) {
|
||||
state =
|
||||
typeof patch === 'function'
|
||||
? patch(state)
|
||||
: { ...state, ...patch };
|
||||
state = typeof patch === 'function' ? patch(state) : { ...state, ...patch };
|
||||
save();
|
||||
for (const fn of subscribers) fn(state);
|
||||
},
|
||||
|
||||
@ -121,7 +121,8 @@ export function importFlags({ seen = [], caught = [], game } = {}) {
|
||||
const stamp = new Date().toISOString();
|
||||
const bucket = { ...(s.games[vgKey] || {}) };
|
||||
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 } };
|
||||
});
|
||||
}
|
||||
@ -140,10 +141,7 @@ export function replaceFlags({ seen = [], caught = [], game } = {}) {
|
||||
|
||||
/** Aggregate seen/caught over a species list for one game (union when 'all'). */
|
||||
export function stats(speciesIds, vgKey = curGame()) {
|
||||
const get =
|
||||
vgKey === 'all'
|
||||
? unionEntry
|
||||
: (id) => selection.get().games[vgKey]?.[id] || GBLANK;
|
||||
const get = vgKey === 'all' ? unionEntry : (id) => selection.get().games[vgKey]?.[id] || GBLANK;
|
||||
let seen = 0;
|
||||
let caught = 0;
|
||||
for (const id of speciesIds) {
|
||||
|
||||
@ -51,13 +51,10 @@ export function applyTheme(state = settings.get()) {
|
||||
if (meta) {
|
||||
const dark =
|
||||
DARKISH.has(theme) ||
|
||||
((!theme || theme === 'system') &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
((!theme || theme === 'system') && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
meta.setAttribute(
|
||||
'content',
|
||||
dark
|
||||
? '#0b0b0c'
|
||||
: getComputedStyle(root).getPropertyValue('--accent').trim() || '#b3161a',
|
||||
dark ? '#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. */
|
||||
export function prefersReducedMotion() {
|
||||
return (
|
||||
!!settings.get().reduceMotion ||
|
||||
window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
!!settings.get().reduceMotion || window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
);
|
||||
}
|
||||
|
||||
@ -14,7 +14,15 @@ export function addHunt({ speciesId, method, charm }) {
|
||||
shinyHunts.set((s) => ({
|
||||
...s,
|
||||
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,
|
||||
],
|
||||
}));
|
||||
|
||||
@ -41,5 +41,8 @@ export function clearTeam() {
|
||||
}
|
||||
|
||||
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;
|
||||
text-decoration: none;
|
||||
line-height: 1;
|
||||
transition: color 0.12s ease, background 0.12s ease;
|
||||
transition:
|
||||
color 0.12s ease,
|
||||
background 0.12s ease;
|
||||
}
|
||||
.reflink:hover {
|
||||
color: var(--text);
|
||||
@ -226,24 +228,64 @@
|
||||
background: var(--type-normal);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.type-chip[data-type="normal"] { background: var(--type-normal); }
|
||||
.type-chip[data-type="fire"] { background: var(--type-fire); }
|
||||
.type-chip[data-type="water"] { background: var(--type-water); }
|
||||
.type-chip[data-type="electric"] { background: var(--type-electric); color: #3a3a1a; }
|
||||
.type-chip[data-type="grass"] { background: var(--type-grass); }
|
||||
.type-chip[data-type="ice"] { 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; }
|
||||
.type-chip[data-type='normal'] {
|
||||
background: var(--type-normal);
|
||||
}
|
||||
.type-chip[data-type='fire'] {
|
||||
background: var(--type-fire);
|
||||
}
|
||||
.type-chip[data-type='water'] {
|
||||
background: var(--type-water);
|
||||
}
|
||||
.type-chip[data-type='electric'] {
|
||||
background: var(--type-electric);
|
||||
color: #3a3a1a;
|
||||
}
|
||||
.type-chip[data-type='grass'] {
|
||||
background: var(--type-grass);
|
||||
}
|
||||
.type-chip[data-type='ice'] {
|
||||
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 ------------------------------------- */
|
||||
.feed-head {
|
||||
@ -477,13 +519,21 @@
|
||||
box-shadow: var(--shadow);
|
||||
content-visibility: auto;
|
||||
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-delay: calc(var(--i, 0) * 18ms);
|
||||
}
|
||||
@keyframes cardIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: none; }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
.card:hover {
|
||||
transform: translateY(-4px);
|
||||
@ -532,11 +582,11 @@
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 8px 12px rgba(0, 0, 0, 0.28));
|
||||
}
|
||||
.card[data-sprite="default"] .card__art .sprite,
|
||||
.card[data-sprite="game"] .card__art .sprite {
|
||||
.card[data-sprite='default'] .card__art .sprite,
|
||||
.card[data-sprite='game'] .card__art .sprite {
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
.card[data-sprite="game"] .card__art .sprite {
|
||||
.card[data-sprite='game'] .card__art .sprite {
|
||||
width: 92px;
|
||||
height: 92px;
|
||||
}
|
||||
@ -548,7 +598,9 @@
|
||||
padding: 5px;
|
||||
border-radius: 12px;
|
||||
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;
|
||||
}
|
||||
.card__body {
|
||||
@ -606,15 +658,15 @@
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
.card__caught[aria-pressed="true"] {
|
||||
.card__caught[aria-pressed='true'] {
|
||||
background: var(--good);
|
||||
color: #fff;
|
||||
}
|
||||
.card__caught[aria-pressed="true"]::before {
|
||||
content: "✓";
|
||||
.card__caught[aria-pressed='true']::before {
|
||||
content: '✓';
|
||||
}
|
||||
.card__caught[aria-pressed="false"]::before {
|
||||
content: "+";
|
||||
.card__caught[aria-pressed='false']::before {
|
||||
content: '+';
|
||||
}
|
||||
.card.is-caught {
|
||||
border-color: color-mix(in srgb, var(--good) 45%, var(--border));
|
||||
@ -677,7 +729,9 @@
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
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 {
|
||||
transform: translateY(-3px);
|
||||
@ -710,7 +764,7 @@
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.gamecard--cover::after {
|
||||
content: "";
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.28), transparent 55%);
|
||||
@ -724,7 +778,9 @@
|
||||
}
|
||||
.gamecard--cover.is-active {
|
||||
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 {
|
||||
font-size: 1rem;
|
||||
@ -774,10 +830,16 @@
|
||||
inset: 0;
|
||||
z-index: -2;
|
||||
background:
|
||||
radial-gradient(120% 80% at 80% 0%, color-mix(in srgb, var(--type-2) 55%, transparent), transparent 60%),
|
||||
linear-gradient(160deg,
|
||||
radial-gradient(
|
||||
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) 62%, #000 38%) 100%);
|
||||
color-mix(in srgb, var(--type-main) 62%, #000 38%) 100%
|
||||
);
|
||||
}
|
||||
.phero__ball {
|
||||
position: absolute;
|
||||
@ -791,7 +853,7 @@
|
||||
border-radius: 50%;
|
||||
}
|
||||
.phero__ball::before {
|
||||
content: "";
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -20px;
|
||||
right: -20px;
|
||||
@ -801,7 +863,7 @@
|
||||
background: #fff;
|
||||
}
|
||||
.phero__ball::after {
|
||||
content: "";
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
@ -912,15 +974,23 @@
|
||||
padding: 14px;
|
||||
border-radius: 18px;
|
||||
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 {
|
||||
image-rendering: pixelated;
|
||||
filter: none;
|
||||
}
|
||||
@keyframes pop {
|
||||
from { transform: scale(0.8); opacity: 0; }
|
||||
to { transform: scale(1); opacity: 1; }
|
||||
from {
|
||||
transform: scale(0.8);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.phero__nav {
|
||||
width: 44px;
|
||||
@ -1036,9 +1106,15 @@
|
||||
animation: pnote-fade 1.6s ease forwards;
|
||||
}
|
||||
@keyframes pnote-fade {
|
||||
0% { opacity: 1; }
|
||||
70% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
70% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.pnote__input {
|
||||
width: 100%;
|
||||
@ -1092,8 +1168,14 @@
|
||||
animation: fade 0.25s ease;
|
||||
}
|
||||
@keyframes fade {
|
||||
from { opacity: 0; transform: translateY(4px); }
|
||||
to { opacity: 1; transform: none; }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
.ppanel__flavor {
|
||||
font-size: 1rem;
|
||||
@ -1131,8 +1213,14 @@
|
||||
animation: flavorSlide 0.28s var(--ease-spring);
|
||||
}
|
||||
@keyframes flavorSlide {
|
||||
from { opacity: 0; transform: translateX(14px); }
|
||||
to { opacity: 1; transform: none; }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(14px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
.flavor__cap {
|
||||
display: block;
|
||||
@ -1221,12 +1309,24 @@
|
||||
border-radius: 999px;
|
||||
transition: width 0.7s var(--ease-spring);
|
||||
}
|
||||
.statbar__fill[data-stat="hp"] { background: #ff6b6b; }
|
||||
.statbar__fill[data-stat="attack"] { background: #ffa94d; }
|
||||
.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__fill[data-stat='hp'] {
|
||||
background: #ff6b6b;
|
||||
}
|
||||
.statbar__fill[data-stat='attack'] {
|
||||
background: #ffa94d;
|
||||
}
|
||||
.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 {
|
||||
font-weight: 700;
|
||||
border-top: 1px solid var(--border);
|
||||
@ -1378,7 +1478,7 @@
|
||||
color: var(--text-dim);
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
.moverow[aria-expanded="true"] .moverow__chev {
|
||||
.moverow[aria-expanded='true'] .moverow__chev {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.movebody {
|
||||
@ -1427,9 +1527,15 @@
|
||||
letter-spacing: 0.03em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.moverow__cat[data-cat="physical"] { color: #e0762f; }
|
||||
.moverow__cat[data-cat="special"] { color: #4b8fd6; }
|
||||
.moverow__cat[data-cat="status"] { color: #8a8f98; }
|
||||
.moverow__cat[data-cat='physical'] {
|
||||
color: #e0762f;
|
||||
}
|
||||
.moverow__cat[data-cat='special'] {
|
||||
color: #4b8fd6;
|
||||
}
|
||||
.moverow__cat[data-cat='status'] {
|
||||
color: #8a8f98;
|
||||
}
|
||||
.moverow__stat {
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
@ -1530,7 +1636,9 @@
|
||||
box-shadow: var(--shadow);
|
||||
text-decoration: none;
|
||||
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 {
|
||||
transform: translateX(3px);
|
||||
@ -1704,7 +1812,9 @@
|
||||
}
|
||||
.swatch.is-on {
|
||||
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 ------------------------------------------- */
|
||||
@ -1802,7 +1912,7 @@
|
||||
cursor: grabbing;
|
||||
}
|
||||
.gsheet__head::before {
|
||||
content: "";
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 50%;
|
||||
@ -1868,8 +1978,14 @@
|
||||
animation: viewIn 0.26s ease both;
|
||||
}
|
||||
@keyframes viewIn {
|
||||
from { opacity: 0; transform: translateY(6px); }
|
||||
to { opacity: 1; transform: none; }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
::view-transition-old(root),
|
||||
@ -1894,8 +2010,12 @@
|
||||
animation: shimmer 1.25s linear infinite;
|
||||
}
|
||||
@keyframes shimmer {
|
||||
from { background-position: 220% 0; }
|
||||
to { background-position: -120% 0; }
|
||||
from {
|
||||
background-position: 220% 0;
|
||||
}
|
||||
to {
|
||||
background-position: -120% 0;
|
||||
}
|
||||
}
|
||||
.sk-hero {
|
||||
background: var(--surface-2);
|
||||
@ -1963,7 +2083,7 @@
|
||||
animation: viewIn 0.25s ease both;
|
||||
}
|
||||
.netpill::before {
|
||||
content: "";
|
||||
content: '';
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
@ -2103,10 +2223,10 @@
|
||||
transition: transform 0.15s ease;
|
||||
margin-left: auto;
|
||||
}
|
||||
.feed-tools__toggle[aria-expanded="true"] .feed-tools__chev {
|
||||
.feed-tools__toggle[aria-expanded='true'] .feed-tools__chev {
|
||||
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);
|
||||
min-width: 17px;
|
||||
height: 17px;
|
||||
@ -2314,11 +2434,24 @@
|
||||
border-radius: 6px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.cov__cell.m4 { background: color-mix(in srgb, var(--danger) 60%, transparent); color: #fff; }
|
||||
.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__cell.m4 {
|
||||
background: color-mix(in srgb, var(--danger) 60%, transparent);
|
||||
color: #fff;
|
||||
}
|
||||
.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 {
|
||||
background: color-mix(in srgb, var(--danger) 12%, transparent);
|
||||
box-shadow: inset 3px 0 0 var(--danger);
|
||||
@ -2965,7 +3098,9 @@
|
||||
image-rendering: pixelated;
|
||||
filter: grayscale(1);
|
||||
opacity: 0.6;
|
||||
transition: filter 0.15s, opacity 0.15s;
|
||||
transition:
|
||||
filter 0.15s,
|
||||
opacity 0.15s;
|
||||
}
|
||||
.formdex__tile.is-caught .formdex__sprite {
|
||||
filter: none;
|
||||
@ -2996,7 +3131,9 @@
|
||||
text-align: center;
|
||||
opacity: 0;
|
||||
transform: scale(0.6);
|
||||
transition: opacity 0.15s, transform 0.15s;
|
||||
transition:
|
||||
opacity 0.15s,
|
||||
transform 0.15s;
|
||||
}
|
||||
.formdex__tile.is-caught .formdex__check {
|
||||
opacity: 1;
|
||||
@ -3231,8 +3368,12 @@
|
||||
border-radius: 999px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.prog__key--caught { background: var(--good); }
|
||||
.prog__key--seen { background: color-mix(in srgb, var(--accent) 35%, transparent); }
|
||||
.prog__key--caught {
|
||||
background: var(--good);
|
||||
}
|
||||
.prog__key--seen {
|
||||
background: color-mix(in srgb, var(--accent) 35%, transparent);
|
||||
}
|
||||
|
||||
/* ---- Breeding helper --------------------------------------- */
|
||||
.breedview__body {
|
||||
@ -3403,15 +3544,44 @@
|
||||
/* ================= Micro-interactions ============================ */
|
||||
/* Tactile press feedback — low specificity so any element's own
|
||||
:active / transition wins if it has one. */
|
||||
:where(.button, .seg__btn, .feed-chip, .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,
|
||||
.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,
|
||||
.flavor__pill, .lineup__add, .shiny__step, .prog__row, .tchart__row,
|
||||
.tchart__col, .gamecard):active {
|
||||
:where(
|
||||
.button,
|
||||
.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);
|
||||
}
|
||||
.card:active {
|
||||
@ -3429,8 +3599,13 @@
|
||||
animation: heroFloat 4.2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes heroFloat {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-6px); }
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Sliding thumb behind the active segment (JS sets --seg-i / --seg-n). */
|
||||
@ -3446,7 +3621,9 @@
|
||||
border-radius: 999px;
|
||||
background: var(--surface);
|
||||
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;
|
||||
}
|
||||
.seg__btn {
|
||||
@ -3482,7 +3659,9 @@
|
||||
padding: 20px;
|
||||
transform: translateY(12px) scale(0.97);
|
||||
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 {
|
||||
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-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);
|
||||
|
||||
/* Pokémon type colors */
|
||||
@ -62,7 +62,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] {
|
||||
:root[data-theme='dark'] {
|
||||
--bg: #0b0b0c;
|
||||
--surface: #17171a;
|
||||
--surface-2: #202024;
|
||||
@ -75,7 +75,7 @@
|
||||
}
|
||||
|
||||
/* Pure black — for OLED screens. */
|
||||
:root[data-theme="black"] {
|
||||
:root[data-theme='black'] {
|
||||
--bg: #000000;
|
||||
--surface: #0b0b0c;
|
||||
--surface-2: #17171a;
|
||||
@ -89,7 +89,7 @@
|
||||
}
|
||||
|
||||
/* Sepia — a warm, low-glare light theme. */
|
||||
:root[data-theme="sepia"] {
|
||||
:root[data-theme='sepia'] {
|
||||
--bg: #f2e8d5;
|
||||
--surface: #fbf4e4;
|
||||
--surface-2: #e9dbbe;
|
||||
@ -101,11 +101,26 @@
|
||||
}
|
||||
|
||||
/* Accent overrides — layered on top of any theme. Red is the default. */
|
||||
:root[data-accent="blue"] { --accent: #2f6fed; --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; }
|
||||
:root[data-accent='blue'] {
|
||||
--accent: #2f6fed;
|
||||
--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;
|
||||
|
||||
@ -101,9 +101,7 @@ export async function AbilityDetail(id) {
|
||||
)
|
||||
: null,
|
||||
mons.length > 24 ? filterInput : null,
|
||||
mons.length
|
||||
? grid
|
||||
: el('p', { class: 'detail__muted' }, 'None on record.'),
|
||||
mons.length ? grid : el('p', { class: 'detail__muted' }, 'None on record.'),
|
||||
),
|
||||
);
|
||||
return view;
|
||||
|
||||
@ -9,9 +9,15 @@ import { openPokemonPicker } from '../components/PokemonPicker.js';
|
||||
import { segThumb } from '../lib/anim.js';
|
||||
|
||||
const EGG_LABEL = {
|
||||
water1: 'Water 1', water2: 'Water 2', water3: 'Water 3',
|
||||
ground: 'Field', humanshape: 'Human-Like', indeterminate: 'Amorphous',
|
||||
plant: 'Grass', 'no-eggs': 'Undiscovered', ditto: 'Ditto',
|
||||
water1: 'Water 1',
|
||||
water2: 'Water 2',
|
||||
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 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' });
|
||||
|
||||
@ -125,7 +138,12 @@ export async function BreedingView() {
|
||||
),
|
||||
},
|
||||
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…',
|
||||
);
|
||||
body.append(slot);
|
||||
@ -133,7 +151,13 @@ export async function BreedingView() {
|
||||
|
||||
const tGroups = new Set(target.eggGroups || []);
|
||||
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;
|
||||
}
|
||||
const isDitto = tGroups.has('ditto');
|
||||
@ -141,7 +165,9 @@ export async function BreedingView() {
|
||||
|
||||
let list;
|
||||
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) {
|
||||
list = snap.species.filter((s) => (s.eggGroups || []).includes('ditto'));
|
||||
} else {
|
||||
@ -168,12 +194,17 @@ export async function BreedingView() {
|
||||
if (!prev) break;
|
||||
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)
|
||||
.then((pk) => {
|
||||
const eggMoves = pk.moves.filter((m) =>
|
||||
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);
|
||||
@ -196,12 +227,20 @@ export async function BreedingView() {
|
||||
'div',
|
||||
{ class: 'breed__eggmove-list' },
|
||||
...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(
|
||||
el('p', { class: 'breed__note' }, note),
|
||||
|
||||
@ -95,7 +95,11 @@ export async function CompareView() {
|
||||
body.append(
|
||||
mem.length >= 2
|
||||
? 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',
|
||||
{ class: 'view__header' },
|
||||
el('h1', {}, 'Compare'),
|
||||
el(
|
||||
'p',
|
||||
{},
|
||||
vg ? `Types shown for ${vg.name}.` : 'Types shown for the newest games.',
|
||||
),
|
||||
el('p', {}, vg ? `Types shown for ${vg.name}.` : 'Types shown for the newest games.'),
|
||||
),
|
||||
slots,
|
||||
body,
|
||||
|
||||
@ -10,8 +10,24 @@ import { Sprite } from '../components/Sprite.js';
|
||||
import { typesForGen } from '../lib/type-resolve.js';
|
||||
|
||||
const TYPES = [
|
||||
'normal', 'fire', 'water', 'electric', 'grass', 'ice', 'fighting', 'poison',
|
||||
'ground', 'flying', 'psychic', 'bug', 'rock', 'ghost', 'dragon', 'dark', 'steel', 'fairy',
|
||||
'normal',
|
||||
'fire',
|
||||
'water',
|
||||
'electric',
|
||||
'grass',
|
||||
'ice',
|
||||
'fighting',
|
||||
'poison',
|
||||
'ground',
|
||||
'flying',
|
||||
'psychic',
|
||||
'bug',
|
||||
'rock',
|
||||
'ghost',
|
||||
'dragon',
|
||||
'dark',
|
||||
'steel',
|
||||
'fairy',
|
||||
];
|
||||
|
||||
const FILTERS = [
|
||||
@ -47,30 +63,55 @@ const SEARCH_ICON =
|
||||
function sortValue(row, key) {
|
||||
const sp = row.species;
|
||||
switch (key) {
|
||||
case 'dex': return row.number ?? sp.id;
|
||||
case 'name': return sp.name;
|
||||
case 'bst': return sp.bst ?? 0;
|
||||
case 'hp': case 'atk': case 'def': case 'spa': case 'spd': case 'spe':
|
||||
case 'dex':
|
||||
return row.number ?? sp.id;
|
||||
case 'name':
|
||||
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;
|
||||
case 'height': return sp.height ?? 0;
|
||||
case 'weight': return sp.weight ?? 0;
|
||||
case 'baseExp': return sp.baseExp ?? 0;
|
||||
case 'catchrate': return sp.captureRate ?? 0;
|
||||
case 'caught': return gameEntry(sp.id).updatedAt || '';
|
||||
default: return 0;
|
||||
case 'height':
|
||||
return sp.height ?? 0;
|
||||
case 'weight':
|
||||
return sp.weight ?? 0;
|
||||
case 'baseExp':
|
||||
return sp.baseExp ?? 0;
|
||||
case 'catchrate':
|
||||
return sp.captureRate ?? 0;
|
||||
case 'caught':
|
||||
return gameEntry(sp.id).updatedAt || '';
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function metricLabel(key, sp) {
|
||||
switch (key) {
|
||||
case 'bst': return `BST ${sp.bst}`;
|
||||
case 'hp': case 'atk': case 'def': case 'spa': case 'spd': case 'spe':
|
||||
case 'bst':
|
||||
return `BST ${sp.bst}`;
|
||||
case 'hp':
|
||||
case 'atk':
|
||||
case 'def':
|
||||
case 'spa':
|
||||
case 'spd':
|
||||
case 'spe':
|
||||
return `${STAT_SHORT[key]} ${sp.stats?.[key] ?? 0}`;
|
||||
case 'height': return `${((sp.height ?? 0) / 10).toFixed(1)} m`;
|
||||
case 'weight': return `${((sp.weight ?? 0) / 10).toFixed(1)} kg`;
|
||||
case 'baseExp': return sp.baseExp != null ? `${sp.baseExp} EXP` : null;
|
||||
case 'catchrate': return `Catch ${sp.captureRate ?? '?'}`;
|
||||
default: return null;
|
||||
case 'height':
|
||||
return `${((sp.height ?? 0) / 10).toFixed(1)} m`;
|
||||
case 'weight':
|
||||
return `${((sp.weight ?? 0) / 10).toFixed(1)} kg`;
|
||||
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'),
|
||||
...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(
|
||||
@ -347,7 +390,13 @@ export async function DexGrid() {
|
||||
'div',
|
||||
{ class: `feed-tools${ui.get().toolsOpen ? ' is-open' : ''}` },
|
||||
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' }, abilitySelect, eggGroupSelect, minBstField),
|
||||
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 boxed = st.spriteStyle === 'game' && gen <= 2;
|
||||
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 }) => {
|
||||
if (
|
||||
@ -490,8 +541,7 @@ export async function DexGrid() {
|
||||
ids = rows.map((r) => r.species.id);
|
||||
|
||||
title.textContent = prettify(dex.name || dex.key);
|
||||
gameLabel.textContent =
|
||||
st.versionGroup === 'all' ? 'All games' : prettify(st.versionGroup);
|
||||
gameLabel.textContent = st.versionGroup === 'all' ? 'All games' : prettify(st.versionGroup);
|
||||
refreshMeta();
|
||||
paintGrid();
|
||||
}
|
||||
|
||||
@ -86,7 +86,11 @@ export async function ItemDetail(id) {
|
||||
}
|
||||
if (shown.length > HOLDER_CAP) {
|
||||
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}%)` : ''}`,
|
||||
);
|
||||
}
|
||||
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?.crit_rate) bits.push(`+${d.meta.crit_rate} crit rate`);
|
||||
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) {
|
||||
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',
|
||||
{ class: 'view__header' },
|
||||
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(
|
||||
'div',
|
||||
|
||||
@ -67,17 +67,13 @@ function typesForGeneration(pokemon, gen) {
|
||||
.sort((a, b) => a._gen - b._gen);
|
||||
const era = past.find((p) => p._gen >= gen);
|
||||
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 = [
|
||||
'hp',
|
||||
'attack',
|
||||
'defense',
|
||||
'special-attack',
|
||||
'special-defense',
|
||||
'speed',
|
||||
];
|
||||
const STAT_NAMES = ['hp', 'attack', 'defense', 'special-attack', 'special-defense', 'speed'];
|
||||
|
||||
const NO_ENCOUNTER_DATA = new Set([
|
||||
'scarlet-violet',
|
||||
@ -106,10 +102,7 @@ export async function PokemonDetail(nationalId) {
|
||||
let pokemon;
|
||||
let species;
|
||||
try {
|
||||
[pokemon, species] = await Promise.all([
|
||||
getPokemon(nationalId),
|
||||
getSpecies(nationalId),
|
||||
]);
|
||||
[pokemon, species] = await Promise.all([getPokemon(nationalId), getSpecies(nationalId)]);
|
||||
} catch (err) {
|
||||
clear(view).append(
|
||||
el(
|
||||
@ -272,7 +265,13 @@ export async function PokemonDetail(nationalId) {
|
||||
const on = inTeam(nationalId, teamForm());
|
||||
const full = team.get().members.length >= MAX_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.disabled = !on && full;
|
||||
}
|
||||
@ -361,8 +360,10 @@ export async function PokemonDetail(nationalId) {
|
||||
}),
|
||||
);
|
||||
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
|
||||
? el(
|
||||
'span',
|
||||
@ -388,7 +389,10 @@ export async function PokemonDetail(nationalId) {
|
||||
abilityList.length && vgGen >= 3 ? fact('Abilities', abilitiesNode) : null,
|
||||
fact('Introduced', prettify(species.generation.name)),
|
||||
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,
|
||||
),
|
||||
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)),
|
||||
fact(
|
||||
'Egg groups',
|
||||
eggGroups.includes('Undiscovered') ? "Undiscovered (can't breed)" : eggGroups.join(', ') || '—',
|
||||
eggGroups.includes('Undiscovered')
|
||||
? "Undiscovered (can't breed)"
|
||||
: eggGroups.join(', ') || '—',
|
||||
),
|
||||
fact(
|
||||
'Egg cycles',
|
||||
@ -492,13 +498,19 @@ export async function PokemonDetail(nationalId) {
|
||||
);
|
||||
} else if (NO_ENCOUNTER_DATA.has(st.versionGroup)) {
|
||||
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 {
|
||||
getEncounters(nationalId)
|
||||
.then((data) => locPanel.replaceChildren(Locations(data, vg ? vg.versions : [])))
|
||||
.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".
|
||||
const DEFAULT_SLOT_NAME = { unown: 'A' };
|
||||
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,
|
||||
];
|
||||
const countEl = el('span', { class: 'formdex__count' });
|
||||
@ -582,7 +599,9 @@ export async function PokemonDetail(nationalId) {
|
||||
// ---- Tabs -------------------------------------------
|
||||
const TABS = [
|
||||
{ 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: 'evo', label: 'Evolution', node: evoPanel },
|
||||
{ id: 'moves', label: 'Moves', node: movesPanel },
|
||||
@ -745,7 +764,11 @@ export async function PokemonDetail(nationalId) {
|
||||
{ class: 'phero' },
|
||||
el('div', { class: 'phero__bg' }),
|
||||
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(
|
||||
'div',
|
||||
{ class: 'phero__top' },
|
||||
@ -753,23 +776,34 @@ export async function PokemonDetail(nationalId) {
|
||||
el('span', { class: 'phero__num' }, numLabel),
|
||||
el('div', { class: 'phero__actions' }, cryBtn, shinyBtn, favBtn),
|
||||
),
|
||||
el(
|
||||
'div',
|
||||
{ class: 'phero__head' },
|
||||
heroName,
|
||||
heroTypes,
|
||||
),
|
||||
el('div', { class: 'phero__head' }, heroName, heroTypes),
|
||||
formBar,
|
||||
cosmeticBar,
|
||||
el(
|
||||
'div',
|
||||
{ class: 'phero__stage' },
|
||||
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' }),
|
||||
artHolder,
|
||||
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' }),
|
||||
),
|
||||
),
|
||||
|
||||
@ -53,7 +53,9 @@ export async function ProgressView() {
|
||||
clear(list);
|
||||
|
||||
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;
|
||||
|
||||
// National total — caught in any game.
|
||||
@ -115,7 +117,11 @@ export async function ProgressView() {
|
||||
render();
|
||||
|
||||
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(
|
||||
'header',
|
||||
{ class: 'view__header' },
|
||||
|
||||
@ -9,11 +9,31 @@ import { TypeChip } from '../components/TypeChip.js';
|
||||
import { segThumb } from '../lib/anim.js';
|
||||
|
||||
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 TYPES = [
|
||||
'normal', 'fire', 'water', 'electric', 'grass', 'ice', 'fighting', 'poison',
|
||||
'ground', 'flying', 'psychic', 'bug', 'rock', 'ghost', 'dragon', 'dark', 'steel', 'fairy',
|
||||
'normal',
|
||||
'fire',
|
||||
'water',
|
||||
'electric',
|
||||
'grass',
|
||||
'ice',
|
||||
'fighting',
|
||||
'poison',
|
||||
'ground',
|
||||
'flying',
|
||||
'psychic',
|
||||
'bug',
|
||||
'rock',
|
||||
'ghost',
|
||||
'dragon',
|
||||
'dark',
|
||||
'steel',
|
||||
'fairy',
|
||||
];
|
||||
const itemSprite = (name) =>
|
||||
`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/${name}.png`;
|
||||
@ -30,7 +50,7 @@ const tmSortKey = (label) => {
|
||||
if (!label) return 1e9;
|
||||
const m = String(label).match(/^([A-Z]+)0*(\d+)$/);
|
||||
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() {
|
||||
@ -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 note = el('p', { class: 'search__note' });
|
||||
@ -119,9 +146,7 @@ export async function SearchView() {
|
||||
const vg = settings.get().versionGroup;
|
||||
const gameHasKind = (kind) =>
|
||||
snap.moves.some((m) =>
|
||||
m.machines.some(
|
||||
(x) => (vg === 'all' || x.vg === vg) && (x.tm || '').startsWith(kind),
|
||||
),
|
||||
m.machines.some((x) => (vg === 'all' || x.vg === vg) && (x.tm || '').startsWith(kind)),
|
||||
);
|
||||
const hasTm = gameHasKind('TM');
|
||||
const hasHm = gameHasKind('HM'); // false for Gen 8+ games, which dropped HMs
|
||||
@ -137,13 +162,23 @@ export async function SearchView() {
|
||||
|
||||
filters.append(
|
||||
...[
|
||||
pill('mvType', [['', 'Any type'], ...TYPES.map((t) => [t, prettify(t)])], ui.get().mvType, (v) => {
|
||||
pill(
|
||||
'mvType',
|
||||
[['', 'Any type'], ...TYPES.map((t) => [t, prettify(t)])],
|
||||
ui.get().mvType,
|
||||
(v) => {
|
||||
ui.set({ mvType: v });
|
||||
run();
|
||||
}),
|
||||
},
|
||||
),
|
||||
pill(
|
||||
'mvClass',
|
||||
[['', 'Any category'], ['physical', 'Physical'], ['special', 'Special'], ['status', 'Status']],
|
||||
[
|
||||
['', 'Any category'],
|
||||
['physical', 'Physical'],
|
||||
['special', 'Special'],
|
||||
['status', 'Status'],
|
||||
],
|
||||
ui.get().mvClass,
|
||||
(v) => {
|
||||
ui.set({ mvClass: v });
|
||||
@ -181,16 +216,28 @@ export async function SearchView() {
|
||||
run();
|
||||
},
|
||||
),
|
||||
pill('itSort', [['name', 'A–Z'], ['id', 'Dex order']], ui.get().itSort, (v) => {
|
||||
pill(
|
||||
'itSort',
|
||||
[
|
||||
['name', 'A–Z'],
|
||||
['id', 'Dex order'],
|
||||
],
|
||||
ui.get().itSort,
|
||||
(v) => {
|
||||
ui.set({ itSort: v });
|
||||
run();
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
} else if (tab === 'abilities') {
|
||||
filters.append(
|
||||
pill(
|
||||
'abSort',
|
||||
[['name', 'A–Z'], ['count', 'Most Pokémon'], ['gen', 'Newest']],
|
||||
[
|
||||
['name', 'A–Z'],
|
||||
['count', 'Most Pokémon'],
|
||||
['gen', 'Newest'],
|
||||
],
|
||||
ui.get().abSort,
|
||||
(v) => {
|
||||
ui.set({ abSort: v });
|
||||
@ -228,12 +275,11 @@ export async function SearchView() {
|
||||
const matches = snap.species
|
||||
.filter(
|
||||
(s) =>
|
||||
loose(s.name).includes(q) ||
|
||||
String(s.id) === q ||
|
||||
(s.types || []).some((t) => t === q),
|
||||
loose(s.name).includes(q) || String(s.id) === q || (s.types || []).some((t) => t === q),
|
||||
)
|
||||
.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) {
|
||||
const e = entry(s.id);
|
||||
results.append(
|
||||
@ -289,8 +335,10 @@ export async function SearchView() {
|
||||
});
|
||||
|
||||
list.sort((a, b) => {
|
||||
if (sort === 'power') 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 === 'power')
|
||||
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 === 'tm') {
|
||||
return (
|
||||
@ -318,14 +366,24 @@ export async function SearchView() {
|
||||
'span',
|
||||
{ class: 'search__types' },
|
||||
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` : ''),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
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'}`;
|
||||
const shown = list.slice(0, 400);
|
||||
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 });
|
||||
results.append(
|
||||
el(
|
||||
@ -356,7 +419,13 @@ export async function SearchView() {
|
||||
);
|
||||
}
|
||||
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),
|
||||
);
|
||||
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);
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
@ -14,16 +14,21 @@ export async function SettingsView() {
|
||||
const view = el('section', { class: 'view settings' });
|
||||
const st = settings.get();
|
||||
|
||||
const themeField = selectField('Theme', st.theme, [
|
||||
const themeField = selectField(
|
||||
'Theme',
|
||||
st.theme,
|
||||
[
|
||||
['system', 'System'],
|
||||
['light', 'Light'],
|
||||
['dark', 'Dark'],
|
||||
['black', 'Black (OLED)'],
|
||||
['sepia', 'Sepia'],
|
||||
], (v) => {
|
||||
],
|
||||
(v) => {
|
||||
settings.set({ theme: v });
|
||||
applyTheme();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const ACCENTS = [
|
||||
['red', '#b3161a'],
|
||||
@ -55,16 +60,23 @@ export async function SettingsView() {
|
||||
);
|
||||
const accentField = el('div', { class: 'field' }, el('span', {}, 'Accent color'), accentRow);
|
||||
|
||||
const fontSizeField = selectField('Text size', st.fontScale || 'default', [
|
||||
const fontSizeField = selectField(
|
||||
'Text size',
|
||||
st.fontScale || 'default',
|
||||
[
|
||||
['small', 'Small'],
|
||||
['default', 'Default'],
|
||||
['large', 'Large'],
|
||||
], (v) => {
|
||||
],
|
||||
(v) => {
|
||||
settings.set({ fontScale: v });
|
||||
applyTheme();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const reduceMotionField = el('label', { class: 'field field--check' },
|
||||
const reduceMotionField = el(
|
||||
'label',
|
||||
{ class: 'field field--check' },
|
||||
el('input', {
|
||||
type: 'checkbox',
|
||||
checked: !!st.reduceMotion,
|
||||
@ -76,14 +88,21 @@ export async function SettingsView() {
|
||||
'Reduce motion',
|
||||
);
|
||||
|
||||
const spriteField = selectField('Sprite style', st.spriteStyle, [
|
||||
const spriteField = selectField(
|
||||
'Sprite style',
|
||||
st.spriteStyle,
|
||||
[
|
||||
['default', 'Pixel (modern)'],
|
||||
['game', 'Game era (pixel art from the selected game)'],
|
||||
['official', 'Official artwork'],
|
||||
['home', 'Pokémon HOME'],
|
||||
], (v) => settings.set({ spriteStyle: v }));
|
||||
],
|
||||
(v) => settings.set({ spriteStyle: v }),
|
||||
);
|
||||
|
||||
const shinyField = el('label', { class: 'field field--check' },
|
||||
const shinyField = el(
|
||||
'label',
|
||||
{ class: 'field field--check' },
|
||||
el('input', {
|
||||
type: 'checkbox',
|
||||
checked: st.showShiny,
|
||||
@ -92,7 +111,9 @@ export async function SettingsView() {
|
||||
'Default detail view to shiny sprites',
|
||||
);
|
||||
|
||||
const teamNavField = el('label', { class: 'field field--check' },
|
||||
const teamNavField = el(
|
||||
'label',
|
||||
{ class: 'field field--check' },
|
||||
el('input', {
|
||||
type: 'checkbox',
|
||||
checked: st.showTeamNav !== false,
|
||||
@ -101,7 +122,9 @@ export async function SettingsView() {
|
||||
'Show Team in the navigation bar',
|
||||
);
|
||||
|
||||
const hapticsField = el('label', { class: 'field field--check' },
|
||||
const hapticsField = el(
|
||||
'label',
|
||||
{ class: 'field field--check' },
|
||||
el('input', {
|
||||
type: 'checkbox',
|
||||
checked: st.haptics !== false,
|
||||
@ -110,11 +133,16 @@ export async function SettingsView() {
|
||||
'Haptic feedback on catch',
|
||||
);
|
||||
|
||||
const defaultRouteField = selectField('Landing screen', st.defaultRoute || 'dex', [
|
||||
const defaultRouteField = selectField(
|
||||
'Landing screen',
|
||||
st.defaultRoute || 'dex',
|
||||
[
|
||||
['dex', 'Dex'],
|
||||
['team', 'Team'],
|
||||
['search', 'Search'],
|
||||
], (v) => settings.set({ defaultRoute: v }));
|
||||
],
|
||||
(v) => settings.set({ defaultRoute: v }),
|
||||
);
|
||||
|
||||
const storageNote = el('p', { class: 'settings__note' }, 'Calculating storage…');
|
||||
if (navigator.storage?.estimate) {
|
||||
@ -157,7 +185,11 @@ export async function SettingsView() {
|
||||
body: `${dex.seen.length} seen · ${dex.caught.length} caught in this save.`,
|
||||
choices: [
|
||||
{ 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' },
|
||||
],
|
||||
});
|
||||
@ -253,25 +285,41 @@ export async function SettingsView() {
|
||||
el(
|
||||
'div',
|
||||
{ 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.'),
|
||||
),
|
||||
saveInput,
|
||||
saveNote,
|
||||
storageNote,
|
||||
el('div', { class: 'settings__actions' },
|
||||
el('button', {
|
||||
el(
|
||||
'div',
|
||||
{ class: 'settings__actions' },
|
||||
el(
|
||||
'button',
|
||||
{
|
||||
class: 'button',
|
||||
type: 'button',
|
||||
onclick: () => exportData(),
|
||||
}, 'Export backup'),
|
||||
el('button', {
|
||||
},
|
||||
'Export backup',
|
||||
),
|
||||
el(
|
||||
'button',
|
||||
{
|
||||
class: 'button',
|
||||
type: 'button',
|
||||
onclick: () => importInput.click(),
|
||||
}, 'Import backup'),
|
||||
},
|
||||
'Import backup',
|
||||
),
|
||||
importInput,
|
||||
el('button', {
|
||||
el(
|
||||
'button',
|
||||
{
|
||||
class: 'button button--danger',
|
||||
type: 'button',
|
||||
onclick: () => {
|
||||
@ -279,24 +327,38 @@ export async function SettingsView() {
|
||||
selection.replace({ version: 3, pokemon: {}, games: {} });
|
||||
}
|
||||
},
|
||||
}, 'Clear tracking data'),
|
||||
el('button', {
|
||||
},
|
||||
'Clear tracking data',
|
||||
),
|
||||
el(
|
||||
'button',
|
||||
{
|
||||
class: 'button button--danger',
|
||||
type: 'button',
|
||||
onclick: async () => {
|
||||
if (!('caches' in window)) return;
|
||||
if (confirm('Clear cached PokéAPI data and sprites? They will re-download when online.')) {
|
||||
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('p', { class: 'settings__note' },
|
||||
el(
|
||||
'p',
|
||||
{ class: 'settings__note' },
|
||||
'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.',
|
||||
),
|
||||
);
|
||||
@ -311,9 +373,7 @@ function selectField(label, value, options, onChange) {
|
||||
const select = el(
|
||||
'select',
|
||||
{ onchange: (e) => onChange(e.target.value) },
|
||||
...options.map(([v, text]) =>
|
||||
el('option', { value: v, selected: v === value }, text),
|
||||
),
|
||||
...options.map(([v, text]) => el('option', { value: v, selected: v === value }, text)),
|
||||
);
|
||||
return el('label', { class: 'field' }, el('span', {}, label), select);
|
||||
}
|
||||
|
||||
@ -71,13 +71,28 @@ export async function ShinyView() {
|
||||
'span',
|
||||
{},
|
||||
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(
|
||||
'div',
|
||||
{ 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,
|
||||
el(
|
||||
'button',
|
||||
@ -137,13 +152,25 @@ export async function ShinyView() {
|
||||
}
|
||||
|
||||
function chooseMethod(done) {
|
||||
const backdrop = el('div', { 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 backdrop = el('div', {
|
||||
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 panel = el(
|
||||
'div',
|
||||
{ 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,
|
||||
el('label', { class: 'lookup__check', style: 'margin-top:12px' }, charmChk, 'Shiny Charm'),
|
||||
el(
|
||||
@ -173,17 +200,30 @@ export async function ShinyView() {
|
||||
for (const h of hunts) list.append(huntCard(h));
|
||||
}
|
||||
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(
|
||||
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(
|
||||
'header',
|
||||
{ class: 'view__header' },
|
||||
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,
|
||||
);
|
||||
|
||||
@ -16,12 +16,26 @@ import { openPokemonPicker } from '../components/PokemonPicker.js';
|
||||
import { segThumb } from '../lib/anim.js';
|
||||
|
||||
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) =>
|
||||
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 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() {
|
||||
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 body = el('div', { class: 'teamview__body' });
|
||||
@ -75,9 +96,24 @@ export async function TeamView() {
|
||||
el(
|
||||
'nav',
|
||||
{ class: 'teamview__refs', 'aria-label': 'Reference tools' },
|
||||
el('a', { 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'),
|
||||
el(
|
||||
'a',
|
||||
{ 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,
|
||||
@ -213,9 +249,7 @@ export async function TeamView() {
|
||||
const threats = analysis
|
||||
.filter((a) => a.weakN >= 2 || (a.weakN >= 1 && a.resist === 0))
|
||||
.sort((a, b) => b.weakN - a.weakN || a.resist - b.resist);
|
||||
const unresisted = analysis
|
||||
.filter((a) => a.weakN === 0 && a.resist === 0)
|
||||
.map((a) => a.type);
|
||||
const unresisted = analysis.filter((a) => a.weakN === 0 && a.resist === 0).map((a) => a.type);
|
||||
|
||||
const weakSpots = threats.length
|
||||
? el(
|
||||
@ -282,7 +316,11 @@ export async function TeamView() {
|
||||
el(
|
||||
'span',
|
||||
{ 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(
|
||||
@ -306,7 +344,9 @@ export async function TeamView() {
|
||||
.slice()
|
||||
.sort(
|
||||
(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)),
|
||||
)[0];
|
||||
|
||||
@ -449,7 +489,11 @@ export async function TeamView() {
|
||||
Sprite(sp.spriteId ?? sp.id, { style, size: 48, alt: 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() {
|
||||
@ -482,7 +526,13 @@ export async function TeamView() {
|
||||
),
|
||||
)
|
||||
: 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');
|
||||
@ -550,12 +600,18 @@ export async function TeamView() {
|
||||
const v = forGeneration(raw, gen, genOfVg);
|
||||
moveNote.textContent = `${prettify(v.type)} · ${v.damage_class ? prettify(v.damage_class) : 'Status'}${v.power ? ` · ${v.power} power` : ''}`;
|
||||
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;
|
||||
}
|
||||
if (!v.damage_class || v.damage_class === 'status' || !v.power) {
|
||||
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;
|
||||
}
|
||||
@ -612,13 +668,23 @@ export async function TeamView() {
|
||||
el(
|
||||
'p',
|
||||
{ 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(
|
||||
'div',
|
||||
{ class: 'calc__badges' },
|
||||
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';
|
||||
|
||||
const ABBR = {
|
||||
normal: 'NOR', fire: 'FIR', 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',
|
||||
normal: 'NOR',
|
||||
fire: 'FIR',
|
||||
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 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 hot = selected === atk || selected === def;
|
||||
grid.append(
|
||||
el(
|
||||
'span',
|
||||
{ class: `tchart__cell ${cellClass(m)}${hot ? ' is-hot' : ''}` },
|
||||
cellText(m),
|
||||
),
|
||||
el('span', { class: `tchart__cell ${cellClass(m)}${hot ? ' is-hot' : ''}` }, cellText(m)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -90,7 +101,11 @@ export async function TypeChartView() {
|
||||
clear(summary);
|
||||
if (!selected) {
|
||||
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;
|
||||
}
|
||||
@ -99,7 +114,12 @@ export async function TypeChartView() {
|
||||
const def = defensiveMatchups([selected], gen);
|
||||
const chipRow = (label, list) =>
|
||||
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;
|
||||
summary.append(
|
||||
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.
|
||||
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
|
||||
},
|
||||
includeAssets: [
|
||||
'icon.svg',
|
||||
'favicon.svg',
|
||||
'apple-touch-icon.png',
|
||||
],
|
||||
includeAssets: ['icon.svg', 'favicon.svg', 'apple-touch-icon.png'],
|
||||
manifest: {
|
||||
name: 'Pocketdex',
|
||||
short_name: 'Pocketdex',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user