Compare commits
2 Commits
c1ce972ce9
...
83cb89aedd
| Author | SHA1 | Date | |
|---|---|---|---|
| 83cb89aedd | |||
| a70f3c9976 |
@ -9,7 +9,7 @@ framework.
|
|||||||
| Feature | How it works |
|
| Feature | How it works |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| **Game series selection** | Pick a *game* (PokéAPI version group, e.g. Scarlet & Violet). If it has more than one regional dex (Paldea / Kitakami / Blueberry) a sub-dex switcher appears. The choice drives which species show, their regional numbering, and which version's flavor text the detail page uses. Stored in `localStorage`. |
|
| **Game series selection** | Pick a *game* (PokéAPI version group, e.g. Scarlet & Violet). If it has more than one regional dex (Paldea / Kitakami / Blueberry) a sub-dex switcher appears. The choice drives which species show, their regional numbering, and which version's flavor text the detail page uses. Stored in `localStorage`. |
|
||||||
| **Unified Pokémon selection** | One tracking record per Pokémon, keyed by **National Dex id**. Mark Pikachu once and every game's view reflects it — each dex computes its own "Seen / Caught" totals by intersecting its species list with that single map. `#/progress` breaks that down for every game at once. The active Pokémon is also a single deep-linkable route (`#/pokemon/25`). |
|
| **Unified Pokémon selection** | One tracking record per Pokémon, keyed by **National Dex id**. Mark Pikachu once and every game's view reflects it — each dex computes its own "Seen / Caught" totals by intersecting its species list with that single map. `#/progress` breaks that down per game, leading with the ones you've actually played (selected, or imported a save for) and tucking the rest behind a toggle. The active Pokémon is also a single deep-linkable route (`#/pokemon/25`). |
|
||||||
|
|
||||||
## Data & storage split
|
## Data & storage split
|
||||||
|
|
||||||
|
|||||||
@ -87,6 +87,7 @@ function tryGen1(bytes) {
|
|||||||
return {
|
return {
|
||||||
gen: 1,
|
gen: 1,
|
||||||
game: 'Red / Blue / Yellow',
|
game: 'Red / Blue / Yellow',
|
||||||
|
versionGroups: ['red-blue', 'yellow'],
|
||||||
trainer: decodeName(bytes.subarray(G1.name, G1.name + 11), GB_CHARS, 0x50),
|
trainer: decodeName(bytes.subarray(G1.name, G1.name + 11), GB_CHARS, 0x50),
|
||||||
seen,
|
seen,
|
||||||
caught,
|
caught,
|
||||||
@ -100,8 +101,8 @@ function tryGen1(bytes) {
|
|||||||
* ------------------------------------------------------------------ */
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
const G2 = {
|
const G2 = {
|
||||||
gs: { name: 0x200b, owned: 0x2a4c, seen: 0x2a6c, ck: 0x2d69, ckFrom: 0x2009, ckTo: 0x2d68, label: 'Gold / Silver' },
|
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' },
|
cr: { name: 0x2009, owned: 0x2a27, seen: 0x2a47, ck: 0x2d0d, ckFrom: 0x2009, ckTo: 0x2b82, label: 'Crystal', vgs: ['crystal'] },
|
||||||
};
|
};
|
||||||
const G2_SPECIES = 251;
|
const G2_SPECIES = 251;
|
||||||
|
|
||||||
@ -118,6 +119,7 @@ function tryGen2Variant(bytes, v) {
|
|||||||
return {
|
return {
|
||||||
gen: 2,
|
gen: 2,
|
||||||
game: v.label,
|
game: v.label,
|
||||||
|
versionGroups: v.vgs,
|
||||||
trainer: decodeName(bytes.subarray(v.name, v.name + 11), GB_CHARS, 0x50),
|
trainer: decodeName(bytes.subarray(v.name, v.name + 11), GB_CHARS, 0x50),
|
||||||
seen,
|
seen,
|
||||||
caught,
|
caught,
|
||||||
@ -190,6 +192,7 @@ function tryGen3(bytes) {
|
|||||||
return {
|
return {
|
||||||
gen: 3,
|
gen: 3,
|
||||||
game: magic === 0xb9 ? 'FireRed / LeafGreen' : 'Ruby / Sapphire / Emerald',
|
game: magic === 0xb9 ? 'FireRed / LeafGreen' : 'Ruby / Sapphire / Emerald',
|
||||||
|
versionGroups: magic === 0xb9 ? ['firered-leafgreen'] : ['ruby-sapphire', 'emerald'],
|
||||||
trainer: decodeName(bytes.subarray(s0, s0 + 7), GBA_CHARS, 0xff),
|
trainer: decodeName(bytes.subarray(s0, s0 + 7), GBA_CHARS, 0xff),
|
||||||
seen,
|
seen,
|
||||||
caught,
|
caught,
|
||||||
@ -279,6 +282,7 @@ function tryGen4(raw) {
|
|||||||
return {
|
return {
|
||||||
gen: 4,
|
gen: 4,
|
||||||
game: 'Diamond / Pearl / Platinum · HeartGold / SoulSilver',
|
game: 'Diamond / Pearl / Platinum · HeartGold / SoulSilver',
|
||||||
|
versionGroups: ['diamond-pearl', 'platinum', 'heartgold-soulsilver'],
|
||||||
trainer: '',
|
trainer: '',
|
||||||
seen: best.seen,
|
seen: best.seen,
|
||||||
caught: best.caught,
|
caught: best.caught,
|
||||||
@ -294,7 +298,7 @@ function readGen5Slot(bytes, slotBase, v) {
|
|||||||
const caught = bitsToList(bytes, p + GEN5.caught, GEN5.species);
|
const caught = bitsToList(bytes, p + GEN5.caught, GEN5.species);
|
||||||
const seen = orSeen(bytes, p, GEN5.seen, GEN5.size, GEN5.species);
|
const seen = orSeen(bytes, p, GEN5.seen, GEN5.size, GEN5.species);
|
||||||
if (!seen.length || caught.some((n) => !seen.includes(n))) return null;
|
if (!seen.length || caught.some((n) => !seen.includes(n))) return null;
|
||||||
return { seen, caught, label: v.label };
|
return { seen, caught, label: v.label, vg: v.dex === GEN5.bw.dex ? 'black-white' : 'black-2-white-2' };
|
||||||
}
|
}
|
||||||
|
|
||||||
function tryGen5(raw) {
|
function tryGen5(raw) {
|
||||||
@ -312,6 +316,7 @@ function tryGen5(raw) {
|
|||||||
return {
|
return {
|
||||||
gen: 5,
|
gen: 5,
|
||||||
game: best.label,
|
game: best.label,
|
||||||
|
versionGroups: [best.vg],
|
||||||
trainer: '',
|
trainer: '',
|
||||||
seen: best.seen,
|
seen: best.seen,
|
||||||
caught: best.caught,
|
caught: best.caught,
|
||||||
@ -326,7 +331,7 @@ function tryGen5(raw) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {ArrayBuffer} buffer
|
* @param {ArrayBuffer} buffer
|
||||||
* @returns {{ gen:number, game:string, trainer:string, seen:number[], caught:number[], nationalUnlocked:boolean }}
|
* @returns {{ gen:number, game:string, versionGroups:string[], trainer:string, seen:number[], caught:number[], nationalUnlocked:boolean }}
|
||||||
* @throws {Error} with a user-facing message if it can't be read.
|
* @throws {Error} with a user-facing message if it can't be read.
|
||||||
*/
|
*/
|
||||||
export function parseSaveDex(buffer) {
|
export function parseSaveDex(buffer) {
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { initRouter, rerender } from './router.js';
|
|||||||
import { Nav } from './components/Nav.js';
|
import { Nav } from './components/Nav.js';
|
||||||
import { settings, applyTheme } from './store/settings.js';
|
import { settings, applyTheme } from './store/settings.js';
|
||||||
import { ui } from './store/ui.js';
|
import { ui } from './store/ui.js';
|
||||||
|
import { markPlayed } from './store/playedGames.js';
|
||||||
import { onInstallChange, promptInstall } from './lib/install.js';
|
import { onInstallChange, promptInstall } from './lib/install.js';
|
||||||
|
|
||||||
applyTheme();
|
applyTheme();
|
||||||
@ -30,10 +31,13 @@ if (!location.hash) {
|
|||||||
// game changes so its data (flavour text, learnset, matchups, evolution,
|
// game changes so its data (flavour text, learnset, matchups, evolution,
|
||||||
// abilities, locations, era sprites) matches the new game.
|
// abilities, locations, era sprites) matches the new game.
|
||||||
let lastVersionGroup = settings.get().versionGroup;
|
let lastVersionGroup = settings.get().versionGroup;
|
||||||
|
// Selecting a game counts as having played it (see store/playedGames.js).
|
||||||
|
markPlayed(lastVersionGroup);
|
||||||
settings.subscribe((s) => {
|
settings.subscribe((s) => {
|
||||||
applyTheme();
|
applyTheme();
|
||||||
if (s.versionGroup !== lastVersionGroup) {
|
if (s.versionGroup !== lastVersionGroup) {
|
||||||
lastVersionGroup = s.versionGroup;
|
lastVersionGroup = s.versionGroup;
|
||||||
|
markPlayed(s.versionGroup);
|
||||||
if ((location.hash || '').startsWith('#/pokemon/')) rerender();
|
if ((location.hash || '').startsWith('#/pokemon/')) rerender();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
27
src/store/playedGames.js
Normal file
27
src/store/playedGames.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { createStore } from './createStore.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The set of games (PokéAPI version-group keys) the user has actually
|
||||||
|
* played — imported a save for, or opened the dex of at least once.
|
||||||
|
*
|
||||||
|
* The tracking model is one unified caught record per Pokémon, so every
|
||||||
|
* game technically shows a caught total the moment you mark anything. That
|
||||||
|
* over-reports games you've never touched. `#/progress` uses this set to
|
||||||
|
* lead with the games you've played and tuck the rest away.
|
||||||
|
*/
|
||||||
|
export const playedGames = createStore('pdx.playedGames', { keys: [] });
|
||||||
|
|
||||||
|
export function isPlayed(key) {
|
||||||
|
return playedGames.get().keys.includes(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markPlayed(...keys) {
|
||||||
|
const have = new Set(playedGames.get().keys);
|
||||||
|
const add = keys.filter((k) => k && k !== 'all' && !have.has(k));
|
||||||
|
if (!add.length) return;
|
||||||
|
playedGames.set((s) => ({ ...s, keys: [...s.keys, ...add] }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unmarkPlayed(key) {
|
||||||
|
playedGames.set((s) => ({ ...s, keys: s.keys.filter((k) => k !== key) }));
|
||||||
|
}
|
||||||
@ -34,4 +34,5 @@ export const ui = createStore('pdx.ui', {
|
|||||||
itCat: '',
|
itCat: '',
|
||||||
itSort: 'name',
|
itSort: 'name',
|
||||||
abSort: 'name',
|
abSort: 'name',
|
||||||
|
progShowAll: false,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -112,6 +112,47 @@
|
|||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Row of shortcut chips under a view header (Team → Natures / Type chart / …) */
|
||||||
|
.toollinks {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin: -6px 0 18px;
|
||||||
|
}
|
||||||
|
.toollink {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 7px 13px 7px 10px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
line-height: 1;
|
||||||
|
transition: transform 0.12s ease, border-color 0.12s ease, background 0.12s ease;
|
||||||
|
}
|
||||||
|
.toollink:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: color-mix(in srgb, var(--accent) 10%, var(--surface));
|
||||||
|
}
|
||||||
|
.toollink:active {
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
.toollink__ico {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: color-mix(in srgb, var(--accent) 16%, transparent);
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@ -2888,7 +2929,7 @@
|
|||||||
}
|
}
|
||||||
.formdex {
|
.formdex {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(68px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(88px, 1fr));
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
@ -2897,32 +2938,37 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 2px;
|
gap: 4px;
|
||||||
padding: 8px 4px 6px;
|
padding: 10px 4px 8px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
background: var(--surface-2);
|
background: var(--surface-2);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
filter: grayscale(1);
|
transition: background 0.15s;
|
||||||
opacity: 0.55;
|
|
||||||
transition: filter 0.15s, opacity 0.15s, background 0.15s;
|
|
||||||
}
|
}
|
||||||
.formdex__tile.is-caught {
|
.formdex__tile.is-caught {
|
||||||
filter: none;
|
|
||||||
opacity: 1;
|
|
||||||
background: color-mix(in srgb, var(--type-main) 16%, var(--surface-2));
|
background: color-mix(in srgb, var(--type-main) 16%, var(--surface-2));
|
||||||
}
|
}
|
||||||
|
/* Dim only the artwork until caught — the label stays fully legible. */
|
||||||
.formdex__sprite {
|
.formdex__sprite {
|
||||||
width: 40px;
|
width: 60px;
|
||||||
height: 40px;
|
height: 60px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
|
filter: grayscale(1);
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: filter 0.15s, opacity 0.15s;
|
||||||
|
}
|
||||||
|
.formdex__tile.is-caught .formdex__sprite {
|
||||||
|
filter: none;
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
.formdex__label {
|
.formdex__label {
|
||||||
font-size: 0.62rem;
|
font-size: 0.7rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
line-height: 1.15;
|
line-height: 1.15;
|
||||||
|
color: var(--text);
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@ -3104,6 +3150,32 @@
|
|||||||
cursor: default;
|
cursor: default;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
.prog__row--played {
|
||||||
|
box-shadow: inset 3px 0 0 var(--accent);
|
||||||
|
}
|
||||||
|
.prog__toggle {
|
||||||
|
align-self: flex-start;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text-dim);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.prog__toggle:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.prog__rest {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
.prog__name {
|
.prog__name {
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|||||||
@ -511,8 +511,12 @@ export async function PokemonDetail(nationalId) {
|
|||||||
// because you switched your active game to browse something else.
|
// because you switched your active game to browse something else.
|
||||||
let formDexPanel = null;
|
let formDexPanel = null;
|
||||||
if (cosmeticForms.length > 1) {
|
if (cosmeticForms.length > 1) {
|
||||||
|
// For letter/pattern sets the base sprite *is* the first entry (Unown's
|
||||||
|
// default artwork is the "A" shape), so give it that name rather than a
|
||||||
|
// generic "Default".
|
||||||
|
const DEFAULT_SLOT_NAME = { unown: 'A' };
|
||||||
const slots = [
|
const slots = [
|
||||||
{ slug: snapSpecies.name, id: nationalId, name: 'Default', sprite: null },
|
{ slug: snapSpecies.name, id: nationalId, name: DEFAULT_SLOT_NAME[snapSpecies.name] || 'Default', sprite: null },
|
||||||
...cosmeticForms,
|
...cosmeticForms,
|
||||||
];
|
];
|
||||||
const countEl = el('span', { class: 'formdex__count' });
|
const countEl = el('span', { class: 'formdex__count' });
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import { el, clear, onTeardown } from '../lib/dom.js';
|
|||||||
import { loadSnapshot } from '../data/snapshot.js';
|
import { loadSnapshot } from '../data/snapshot.js';
|
||||||
import { settings } from '../store/settings.js';
|
import { settings } from '../store/settings.js';
|
||||||
import { selection, stats } from '../store/selection.js';
|
import { selection, stats } from '../store/selection.js';
|
||||||
|
import { ui } from '../store/ui.js';
|
||||||
|
import { playedGames, isPlayed } from '../store/playedGames.js';
|
||||||
import { dexesForVersionGroup, versionGroupsByGeneration } from '../data/pokedex-resolver.js';
|
import { dexesForVersionGroup, versionGroupsByGeneration } from '../data/pokedex-resolver.js';
|
||||||
|
|
||||||
function bar(seen, caught, total) {
|
function bar(seen, caught, total) {
|
||||||
@ -28,11 +30,33 @@ export async function ProgressView() {
|
|||||||
return [...ids];
|
return [...ids];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function gameRow(vg, { played }) {
|
||||||
|
const s = stats(speciesIdsFor(vg.key));
|
||||||
|
return el(
|
||||||
|
'button',
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
class: `prog__row${played ? ' prog__row--played' : ''}`,
|
||||||
|
onclick: () => {
|
||||||
|
settings.set({ versionGroup: vg.key, pokedex: null });
|
||||||
|
location.hash = '#/';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
el('span', { class: 'prog__name' }, vg.name),
|
||||||
|
bar(s.seen, s.caught, s.total),
|
||||||
|
el('span', { class: 'prog__count' }, `${s.caught} / ${s.total}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const list = el('div', { class: 'prog' });
|
const list = el('div', { class: 'prog' });
|
||||||
function render() {
|
function render() {
|
||||||
clear(list);
|
clear(list);
|
||||||
|
|
||||||
// National total
|
const gens = versionGroupsByGeneration(snap);
|
||||||
|
const anyPlayed = gens.some(({ versionGroups }) => versionGroups.some((vg) => isPlayed(vg.key)));
|
||||||
|
const showAll = ui.get().progShowAll || !anyPlayed;
|
||||||
|
|
||||||
|
// National total — always shown.
|
||||||
const nat = stats(snap.species.map((s) => s.id));
|
const nat = stats(snap.species.map((s) => s.id));
|
||||||
list.append(
|
list.append(
|
||||||
el(
|
el(
|
||||||
@ -44,27 +68,48 @@ export async function ProgressView() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const { generation, versionGroups } of versionGroupsByGeneration(snap)) {
|
// Games you've played (imported a save for, or opened at least once).
|
||||||
list.append(el('h3', { class: 'prog__gen' }, `Gen ${generation.id} · ${generation.name}`));
|
if (anyPlayed) {
|
||||||
for (const vg of versionGroups) {
|
list.append(el('h3', { class: 'prog__gen' }, 'Your games'));
|
||||||
const s = stats(speciesIdsFor(vg.key));
|
for (const { versionGroups } of gens) {
|
||||||
|
for (const vg of versionGroups) {
|
||||||
|
if (isPlayed(vg.key)) list.append(gameRow(vg, { played: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything else — behind a toggle, since a single unified caught
|
||||||
|
// record makes every game report a total whether or not you've touched it.
|
||||||
|
const rest = [];
|
||||||
|
let restCount = 0;
|
||||||
|
for (const { generation, versionGroups } of gens) {
|
||||||
|
const unplayed = versionGroups.filter((vg) => !isPlayed(vg.key));
|
||||||
|
if (!unplayed.length) continue;
|
||||||
|
rest.push(el('h3', { class: 'prog__gen' }, `Gen ${generation.id} · ${generation.name}`));
|
||||||
|
for (const vg of unplayed) {
|
||||||
|
rest.push(gameRow(vg, { played: false }));
|
||||||
|
restCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (restCount) {
|
||||||
|
if (anyPlayed) {
|
||||||
list.append(
|
list.append(
|
||||||
el(
|
el(
|
||||||
'button',
|
'button',
|
||||||
{
|
{
|
||||||
type: 'button',
|
type: 'button',
|
||||||
class: 'prog__row',
|
class: 'prog__toggle',
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
settings.set({ versionGroup: vg.key, pokedex: null });
|
ui.set({ progShowAll: !ui.get().progShowAll });
|
||||||
location.hash = '#/';
|
render();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
el('span', { class: 'prog__name' }, vg.name),
|
showAll ? 'Hide other games' : `Show all other games (${restCount})`,
|
||||||
bar(s.seen, s.caught, s.total),
|
|
||||||
el('span', { class: 'prog__count' }, `${s.caught} / ${s.total}`),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (showAll) list.append(el('div', { class: 'prog__rest' }, ...rest));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
render();
|
render();
|
||||||
@ -75,7 +120,7 @@ export async function ProgressView() {
|
|||||||
'header',
|
'header',
|
||||||
{ class: 'view__header' },
|
{ class: 'view__header' },
|
||||||
el('h1', {}, 'Progress by game'),
|
el('h1', {}, 'Progress by game'),
|
||||||
el('p', {}, 'Your one unified caught record, counted against each game’s dex. Tap a game to switch to it.'),
|
el('p', {}, 'Your one unified caught record, counted against each game’s dex. Games you haven’t played are tucked away. Tap a game to switch to it.'),
|
||||||
),
|
),
|
||||||
el(
|
el(
|
||||||
'p',
|
'p',
|
||||||
@ -88,7 +133,11 @@ export async function ProgressView() {
|
|||||||
list,
|
list,
|
||||||
);
|
);
|
||||||
|
|
||||||
const off = selection.subscribe(render);
|
const offSel = selection.subscribe(render);
|
||||||
onTeardown(view, () => off());
|
const offPlayed = playedGames.subscribe(render);
|
||||||
|
onTeardown(view, () => {
|
||||||
|
offSel();
|
||||||
|
offPlayed();
|
||||||
|
});
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { chooseDialog } from '../lib/dialog.js';
|
|||||||
import { team } from '../store/team.js';
|
import { team } from '../store/team.js';
|
||||||
import { formTracking } from '../store/formTracking.js';
|
import { formTracking } from '../store/formTracking.js';
|
||||||
import { shinyHunts } from '../store/shinyHunts.js';
|
import { shinyHunts } from '../store/shinyHunts.js';
|
||||||
|
import { playedGames, markPlayed } from '../store/playedGames.js';
|
||||||
import { parseSaveDex } from '../lib/savedex.js';
|
import { parseSaveDex } from '../lib/savedex.js';
|
||||||
import { canInstall, onInstallChange, promptInstall } from '../lib/install.js';
|
import { canInstall, onInstallChange, promptInstall } from '../lib/install.js';
|
||||||
|
|
||||||
@ -154,9 +155,11 @@ export async function SettingsView() {
|
|||||||
});
|
});
|
||||||
if (choice === 'merge') {
|
if (choice === 'merge') {
|
||||||
importFlags({ seen: dex.seen, caught: dex.caught });
|
importFlags({ seen: dex.seen, caught: dex.caught });
|
||||||
|
markPlayed(...(dex.versionGroups || []));
|
||||||
saveNote.textContent = `Added ${dex.caught.length} caught / ${dex.seen.length} seen from ${who}.`;
|
saveNote.textContent = `Added ${dex.caught.length} caught / ${dex.seen.length} seen from ${who}.`;
|
||||||
} else if (choice === 'replace') {
|
} else if (choice === 'replace') {
|
||||||
replaceFlags({ seen: dex.seen, caught: dex.caught });
|
replaceFlags({ seen: dex.seen, caught: dex.caught });
|
||||||
|
markPlayed(...(dex.versionGroups || []));
|
||||||
saveNote.textContent = `Replaced tracking with ${who}'s Pokédex (favourites and notes kept).`;
|
saveNote.textContent = `Replaced tracking with ${who}'s Pokédex (favourites and notes kept).`;
|
||||||
} else {
|
} else {
|
||||||
saveNote.textContent = '';
|
saveNote.textContent = '';
|
||||||
@ -179,6 +182,7 @@ export async function SettingsView() {
|
|||||||
if (data.team) team.replace(data.team);
|
if (data.team) team.replace(data.team);
|
||||||
if (data.formTracking) formTracking.replace(data.formTracking);
|
if (data.formTracking) formTracking.replace(data.formTracking);
|
||||||
if (data.shinyHunts) shinyHunts.replace(data.shinyHunts);
|
if (data.shinyHunts) shinyHunts.replace(data.shinyHunts);
|
||||||
|
if (data.playedGames) playedGames.replace(data.playedGames);
|
||||||
alert('Import complete.');
|
alert('Import complete.');
|
||||||
} catch {
|
} catch {
|
||||||
alert('That file could not be read as a Pokédex backup.');
|
alert('That file could not be read as a Pokédex backup.');
|
||||||
@ -314,6 +318,7 @@ function exportData() {
|
|||||||
team: team.get(),
|
team: team.get(),
|
||||||
formTracking: formTracking.get(),
|
formTracking: formTracking.get(),
|
||||||
shinyHunts: shinyHunts.get(),
|
shinyHunts: shinyHunts.get(),
|
||||||
|
playedGames: playedGames.get(),
|
||||||
};
|
};
|
||||||
const blob = new Blob([JSON.stringify(payload, null, 2)], {
|
const blob = new Blob([JSON.stringify(payload, null, 2)], {
|
||||||
type: 'application/json',
|
type: 'application/json',
|
||||||
|
|||||||
@ -66,17 +66,14 @@ export async function TeamView() {
|
|||||||
'header',
|
'header',
|
||||||
{ class: 'view__header' },
|
{ class: 'view__header' },
|
||||||
el('h1', {}, 'Team'),
|
el('h1', {}, 'Team'),
|
||||||
|
el('p', {}, `Up to ${MAX_TEAM} Pokémon — check weaknesses, coverage and stats.`),
|
||||||
el(
|
el(
|
||||||
'p',
|
'nav',
|
||||||
{},
|
{ class: 'toollinks' },
|
||||||
`Up to ${MAX_TEAM} Pokémon — check weaknesses, coverage and stats. `,
|
el('a', { class: 'toollink', href: '#/natures' }, el('span', { class: 'toollink__ico', 'aria-hidden': 'true' }, '✦'), 'Natures'),
|
||||||
el('a', { class: 'link', href: '#/natures' }, 'Natures'),
|
el('a', { class: 'toollink', href: '#/types' }, el('span', { class: 'toollink__ico', 'aria-hidden': 'true' }, '▦'), 'Type chart'),
|
||||||
' · ',
|
el('a', { class: 'toollink', href: '#/compare' }, el('span', { class: 'toollink__ico', 'aria-hidden': 'true' }, '⇄'), 'Compare'),
|
||||||
el('a', { class: 'link', href: '#/types' }, 'Type chart'),
|
el('a', { class: 'toollink', href: '#/breeding' }, el('span', { class: 'toollink__ico', 'aria-hidden': 'true' }, '⬡'), 'Breeding'),
|
||||||
' · ',
|
|
||||||
el('a', { class: 'link', href: '#/compare' }, 'Compare'),
|
|
||||||
' · ',
|
|
||||||
el('a', { class: 'link', href: '#/breeding' }, 'Breeding'),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
seg,
|
seg,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user