Add a standalone interactive type chart page
New #/types route (linked from the Team header alongside Natures): the full 18x18 attack/defend grid, gen-aware from the selected game — drops Fairy pre-Gen 6, Steel/Dark pre-Gen 2, and applies the Gen 1 / pre-Gen 6 quirks the type-chart module already models. Tap a type on either edge to highlight its row+column and get an offensive + defensive breakdown as type chips. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
This commit is contained in:
parent
262e1a454e
commit
cfa30c987f
@ -118,7 +118,9 @@ Working app with a type-themed UI:
|
|||||||
no items/abilities/weather/terrain). All three follow the selected
|
no items/abilities/weather/terrain). All three follow the selected
|
||||||
game — its generation's type chart, era-accurate typings (pre-Gen-6
|
game — its generation's type chart, era-accurate typings (pre-Gen-6
|
||||||
Clefairy is Normal, etc.), and era move data. Toggleable in the nav
|
Clefairy is Normal, etc.), and era move data. Toggleable in the nav
|
||||||
from Settings.
|
from Settings. Links out to a Natures table and an interactive Type
|
||||||
|
chart (tap a type for its offensive + defensive breakdown; both
|
||||||
|
gen-aware).
|
||||||
- **Settings** — theme (System / Light / Dark / Black / Sepia) + accent
|
- **Settings** — theme (System / Light / Dark / Black / Sepia) + accent
|
||||||
colour, text size, an in-app reduce-motion override (on top of the OS
|
colour, text size, an in-app reduce-motion override (on top of the OS
|
||||||
preference, which is always respected too), sprite style, haptic
|
preference, which is always respected too), sprite style, haptic
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { ItemDetail } from './views/ItemDetail.js';
|
|||||||
import { AbilityDetail } from './views/AbilityDetail.js';
|
import { AbilityDetail } from './views/AbilityDetail.js';
|
||||||
import { TeamView } from './views/TeamView.js';
|
import { TeamView } from './views/TeamView.js';
|
||||||
import { NaturesView } from './views/NaturesView.js';
|
import { NaturesView } from './views/NaturesView.js';
|
||||||
|
import { TypeChartView } from './views/TypeChartView.js';
|
||||||
import { detailSkeleton, lookupSkeleton } from './components/skeletons.js';
|
import { detailSkeleton, lookupSkeleton } from './components/skeletons.js';
|
||||||
import { prefersReducedMotion } from './store/settings.js';
|
import { prefersReducedMotion } from './store/settings.js';
|
||||||
|
|
||||||
@ -19,6 +20,7 @@ const routes = [
|
|||||||
{ pattern: /^#\/ability\/(\d+)$/, view: (m) => AbilityDetail(Number(m[1])), skeleton: lookupSkeleton },
|
{ pattern: /^#\/ability\/(\d+)$/, view: (m) => AbilityDetail(Number(m[1])), skeleton: lookupSkeleton },
|
||||||
{ pattern: /^#\/team$/, view: () => TeamView() },
|
{ pattern: /^#\/team$/, view: () => TeamView() },
|
||||||
{ pattern: /^#\/natures$/, view: () => NaturesView() },
|
{ pattern: /^#\/natures$/, view: () => NaturesView() },
|
||||||
|
{ pattern: /^#\/types$/, view: () => TypeChartView() },
|
||||||
{ pattern: /^#\/search$/, view: () => SearchView() },
|
{ pattern: /^#\/search$/, view: () => SearchView() },
|
||||||
{ pattern: /^#\/settings$/, view: () => SettingsView() },
|
{ pattern: /^#\/settings$/, view: () => SettingsView() },
|
||||||
];
|
];
|
||||||
|
|||||||
@ -2909,3 +2909,101 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: color-mix(in srgb, var(--type-main) 55%, var(--text-dim));
|
color: color-mix(in srgb, var(--type-main) 55%, var(--text-dim));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Type chart page ---------------------------------------- */
|
||||||
|
.tchart__scroll {
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
.tchart {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2.6rem repeat(var(--n), 1.9rem);
|
||||||
|
gap: 2px;
|
||||||
|
min-width: max-content;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
.tchart__corner {
|
||||||
|
font-size: 0.5rem;
|
||||||
|
line-height: 1.1;
|
||||||
|
white-space: pre;
|
||||||
|
color: var(--text-dim);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.tchart__col,
|
||||||
|
.tchart__row {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.56rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
min-height: 1.9rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--text);
|
||||||
|
background: color-mix(in srgb, var(--tc) 26%, var(--surface-2));
|
||||||
|
}
|
||||||
|
.tchart__col.is-sel,
|
||||||
|
.tchart__row.is-sel {
|
||||||
|
background: var(--tc);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.tchart__cell {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
min-height: 1.9rem;
|
||||||
|
font-size: 0.66rem;
|
||||||
|
font-weight: 700;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--surface-2);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.tchart__cell.m2 {
|
||||||
|
background: color-mix(in srgb, var(--danger) 42%, transparent);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.tchart__cell.m05 {
|
||||||
|
background: color-mix(in srgb, var(--good) 32%, transparent);
|
||||||
|
}
|
||||||
|
.tchart__cell.m0 {
|
||||||
|
background: color-mix(in srgb, var(--text-dim) 35%, var(--surface-2));
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
.tchart__cell.is-hot {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
.tchart__legend {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
.tchart__legend .tchart__cell {
|
||||||
|
width: 1.5rem;
|
||||||
|
min-height: 1.5rem;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
.tchart__summary {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
.tchart__srow {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: baseline;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.tchart__slabel {
|
||||||
|
font-size: 0.76rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-dim);
|
||||||
|
flex: none;
|
||||||
|
min-width: 9rem;
|
||||||
|
}
|
||||||
|
|||||||
@ -72,7 +72,9 @@ export async function TeamView() {
|
|||||||
'p',
|
'p',
|
||||||
{},
|
{},
|
||||||
`Up to ${MAX_TEAM} Pokémon — check weaknesses, coverage and stats. `,
|
`Up to ${MAX_TEAM} Pokémon — check weaknesses, coverage and stats. `,
|
||||||
el('a', { class: 'link', href: '#/natures' }, 'Natures reference'),
|
el('a', { class: 'link', href: '#/natures' }, 'Natures'),
|
||||||
|
' · ',
|
||||||
|
el('a', { class: 'link', href: '#/types' }, 'Type chart'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
seg,
|
seg,
|
||||||
|
|||||||
146
src/views/TypeChartView.js
Normal file
146
src/views/TypeChartView.js
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import { el, clear } from '../lib/dom.js';
|
||||||
|
import { settings } from '../store/settings.js';
|
||||||
|
import { loadSnapshot } from '../data/snapshot.js';
|
||||||
|
import { prettify } from '../data/pokedex-resolver.js';
|
||||||
|
import { TYPES, multiplier, offensiveSummary, defensiveMatchups } from '../data/type-chart.js';
|
||||||
|
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',
|
||||||
|
};
|
||||||
|
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');
|
||||||
|
|
||||||
|
export async function TypeChartView() {
|
||||||
|
const view = el('section', { class: 'view lookup tchartview' });
|
||||||
|
const snap = await loadSnapshot();
|
||||||
|
const st = settings.get();
|
||||||
|
const vg = snap.versionGroupByKey.get(st.versionGroup);
|
||||||
|
const gen = vg ? vg.generation : 9;
|
||||||
|
|
||||||
|
// Fairy is Gen 6+, Steel/Dark are Gen 2+.
|
||||||
|
const types = TYPES.filter((t) => {
|
||||||
|
if (t === 'fairy' && gen < 6) return false;
|
||||||
|
if ((t === 'steel' || t === 'dark') && gen < 2) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
let selected = null; // a type name, or null
|
||||||
|
const grid = el('div', { class: 'tchart' });
|
||||||
|
const summary = el('div', { class: 'tchart__summary' });
|
||||||
|
|
||||||
|
function select(t) {
|
||||||
|
selected = selected === t ? null : t;
|
||||||
|
paint();
|
||||||
|
}
|
||||||
|
|
||||||
|
function paint() {
|
||||||
|
grid.style.setProperty('--n', String(types.length));
|
||||||
|
clear(grid);
|
||||||
|
|
||||||
|
// corner
|
||||||
|
grid.append(el('span', { class: 'tchart__corner' }, 'ATK→\nDEF↓'));
|
||||||
|
// column headers (defending)
|
||||||
|
for (const def of types) {
|
||||||
|
grid.append(
|
||||||
|
el(
|
||||||
|
'button',
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
class: `tchart__col${selected === def ? ' is-sel' : ''}`,
|
||||||
|
style: `--tc:${typeHex(def)}`,
|
||||||
|
title: prettify(def),
|
||||||
|
onclick: () => select(def),
|
||||||
|
},
|
||||||
|
ABBR[def],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// rows (attacking)
|
||||||
|
for (const atk of types) {
|
||||||
|
grid.append(
|
||||||
|
el(
|
||||||
|
'button',
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
class: `tchart__row${selected === atk ? ' is-sel' : ''}`,
|
||||||
|
style: `--tc:${typeHex(atk)}`,
|
||||||
|
title: prettify(atk),
|
||||||
|
onclick: () => select(atk),
|
||||||
|
},
|
||||||
|
ABBR[atk],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
for (const def of types) {
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.'),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const inEra = (list) => (list || []).filter((t) => types.includes(t));
|
||||||
|
const off = offensiveSummary([selected], gen);
|
||||||
|
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)))
|
||||||
|
: null;
|
||||||
|
summary.append(
|
||||||
|
el('h3', { class: 'ppanel__sub' }, `${prettify(selected)} — attacking`),
|
||||||
|
chipRow('Super effective vs', inEra(off.strong)),
|
||||||
|
chipRow('Not very effective / no effect vs', inEra(off.walls)),
|
||||||
|
el('h3', { class: 'ppanel__sub' }, `${prettify(selected)} — defending`),
|
||||||
|
chipRow('Weak to', inEra(def['2'])),
|
||||||
|
chipRow('Resists', inEra(def['0.5'])),
|
||||||
|
chipRow('Immune to', inEra(def['0'])),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
paint();
|
||||||
|
|
||||||
|
clear(view).append(
|
||||||
|
el('nav', { class: 'lookup__nav' }, el('a', { class: 'link', href: '#/team' }, '‹ Team')),
|
||||||
|
el(
|
||||||
|
'header',
|
||||||
|
{ class: 'view__header' },
|
||||||
|
el('h1', {}, 'Type chart'),
|
||||||
|
el(
|
||||||
|
'p',
|
||||||
|
{},
|
||||||
|
st.versionGroup === 'all'
|
||||||
|
? 'Modern rules (Gen 6+).'
|
||||||
|
: `${prettify(st.versionGroup)} — Gen ${gen} rules${
|
||||||
|
gen < 6 ? ', no Fairy' : ''
|
||||||
|
}${gen < 2 ? ', no Steel/Dark' : ''}.`,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
el(
|
||||||
|
'div',
|
||||||
|
{ class: 'tchart__legend' },
|
||||||
|
el('span', { class: 'tchart__cell m2' }, '2'),
|
||||||
|
' super effective ',
|
||||||
|
el('span', { class: 'tchart__cell m05' }, '½'),
|
||||||
|
' resisted ',
|
||||||
|
el('span', { class: 'tchart__cell m0' }, '0'),
|
||||||
|
' no effect',
|
||||||
|
),
|
||||||
|
el('div', { class: 'tchart__scroll' }, grid),
|
||||||
|
summary,
|
||||||
|
);
|
||||||
|
return view;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user