Team builder: game-aware coverage + a weak-spots summary
Coverage was hardcoded to Gen 9. Now it reads the selected game: only that generation's types get rows (no Fairy pre-Gen 6, no Steel/Dark pre-Gen 2), member typings are era-accurate via the snapshot's pastTypes, and the type chart / STAB maths run at the game's generation. Compare's Type row follows suit. The panel re-renders on a game switch. New "Weak spots" block above the grid: types that hit 2+ members super-effectively, or that a member is weak to and nobody resists (those get a "0 resist" flag), each listing the exposed members. Plus a "no resist to" line for types the whole team only takes neutrally but can't wall. The grid itself swaps the plain "weak" count for a diverging resist/weak bar and tints rows where 2+ members are weak. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
This commit is contained in:
parent
99d472f469
commit
c7a2a821fa
@ -93,8 +93,13 @@ Working app with a type-themed UI:
|
||||
this game" and sort by power / accuracy / recency; Items filter by
|
||||
category. Each has its own detail page. Query, scroll, tab and filters
|
||||
persist.
|
||||
- **Team** — a lineup of up to 6 with a Coverage table (weaknesses, STAB
|
||||
gaps) and a Compare table; toggleable in the nav from Settings.
|
||||
- **Team** — a lineup of up to 6. Coverage leads with a "weak spots"
|
||||
summary (types that hit 2+ members, or that someone's weak to and nobody
|
||||
resists), then a full matchup grid with a diverging resist◀│▶weak bar per
|
||||
type; plus offensive STAB gaps and at-a-glance stats. A Compare table
|
||||
stacks the six side by side. All matchup maths follow the selected game —
|
||||
its generation's type chart and era-accurate typings (pre-Gen-6 Clefairy
|
||||
is Normal, etc.). Toggleable in the nav from Settings.
|
||||
- **Settings** — theme (System / Light / Dark / Black / Sepia) + accent
|
||||
colour, sprite style, JSON export/import.
|
||||
- **PWA** — Workbox SW: precache shell + snapshot, SWR for API JSON,
|
||||
|
||||
@ -2139,18 +2139,128 @@
|
||||
.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__sum {
|
||||
width: 42px;
|
||||
.cov__row--threat {
|
||||
background: color-mix(in srgb, var(--danger) 12%, transparent);
|
||||
box-shadow: inset 3px 0 0 var(--danger);
|
||||
border-radius: 6px;
|
||||
}
|
||||
/* diverging bar: resists grow left of centre, weaknesses grow right */
|
||||
.cov__bal {
|
||||
width: 138px;
|
||||
flex: none;
|
||||
text-align: center;
|
||||
font-size: 0.78rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.cov__bal--head {
|
||||
font-size: 0.66rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-dim);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
justify-content: center;
|
||||
}
|
||||
.cov__sum.is-bad {
|
||||
color: var(--danger);
|
||||
.cov__bal-side {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.cov__bal-side--l {
|
||||
justify-content: flex-end;
|
||||
border-right: 1px solid var(--border);
|
||||
padding-right: 3px;
|
||||
}
|
||||
.cov__bal-side--r {
|
||||
padding-left: 3px;
|
||||
}
|
||||
.cov__bal-res,
|
||||
.cov__bal-weak {
|
||||
height: 12px;
|
||||
border-radius: 3px;
|
||||
min-width: 0;
|
||||
}
|
||||
.cov__bal-res {
|
||||
background: var(--good);
|
||||
}
|
||||
.cov__bal-weak {
|
||||
background: var(--danger);
|
||||
color: #fff;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 800;
|
||||
line-height: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ---- Weak-spots summary ------------------------------------- */
|
||||
.cov__ok {
|
||||
color: var(--good);
|
||||
font-weight: 600;
|
||||
margin: 4px 0 2px;
|
||||
}
|
||||
.weakspots {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.weakspot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
padding: 7px 10px;
|
||||
border-radius: 12px;
|
||||
background: color-mix(in srgb, var(--danger) 10%, var(--surface-2));
|
||||
}
|
||||
.weakspot.is-nores {
|
||||
background: color-mix(in srgb, var(--danger) 20%, var(--surface-2));
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--danger) 45%, transparent);
|
||||
}
|
||||
.weakspot__count {
|
||||
font-weight: 800;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.weakspot__flag {
|
||||
font-size: 0.68rem;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: #fff;
|
||||
background: var(--danger);
|
||||
padding: 2px 6px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
.weakspot__res {
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-dim);
|
||||
font-weight: 600;
|
||||
}
|
||||
.weakspot__mem {
|
||||
display: flex;
|
||||
gap: 3px;
|
||||
margin-left: auto;
|
||||
}
|
||||
.weakspot__chip {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
.weakspot__chip .sprite {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.cov__unres {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
margin: 2px 0 4px;
|
||||
}
|
||||
.cov__unres-k {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
/* ---- Compare table ------------------------------------------- */
|
||||
|
||||
@ -4,6 +4,7 @@ import { settings } from '../store/settings.js';
|
||||
import { ui } from '../store/ui.js';
|
||||
import { team, addToTeam, removeFromTeam, clearTeam, MAX_TEAM } from '../store/team.js';
|
||||
import { TYPES, defenseVector, multiplier } from '../data/type-chart.js';
|
||||
import { typesForGen } from '../lib/type-resolve.js';
|
||||
import { Sprite } from '../components/Sprite.js';
|
||||
import { TypeChip } from '../components/TypeChip.js';
|
||||
import { openPokemonPicker } from '../components/PokemonPicker.js';
|
||||
@ -143,38 +144,124 @@ export async function TeamView() {
|
||||
}
|
||||
|
||||
function coverage(mem) {
|
||||
const gen = 9;
|
||||
const vectors = mem.map((sp) => defenseVector(sp.types, gen));
|
||||
const st = settings.get();
|
||||
const vg = snap.versionGroupByKey.get(st.versionGroup);
|
||||
const gen = vg ? vg.generation : 9;
|
||||
const gameLabel = st.versionGroup === 'all' ? 'all games' : prettify(st.versionGroup);
|
||||
const style = st.spriteStyle;
|
||||
|
||||
// Defensive weakness table
|
||||
// Only the types that exist in the selected game (no Fairy before Gen 6,
|
||||
// no Steel/Dark before Gen 2). Member typings are era-accurate too —
|
||||
// pre-Gen-6 Clefairy is Normal, Gen 1 Magnemite is pure Electric.
|
||||
const GEN_TYPES = TYPES.filter((t) => {
|
||||
if (t === 'fairy' && gen < 6) return false;
|
||||
if ((t === 'steel' || t === 'dark') && gen < 2) return false;
|
||||
return true;
|
||||
});
|
||||
const memTypes = mem.map((sp) => typesForGen(sp, gen));
|
||||
const vectors = memTypes.map((types) => defenseVector(types, gen));
|
||||
|
||||
// Per attacking type: which members are weak, how many resist.
|
||||
const analysis = GEN_TYPES.map((t) => {
|
||||
const weak = [];
|
||||
let resist = 0;
|
||||
vectors.forEach((v, i) => {
|
||||
const m = v[t] ?? 1;
|
||||
if (m >= 2) weak.push({ sp: mem[i], m });
|
||||
else if (m < 1) resist += 1;
|
||||
});
|
||||
return { type: t, weak, weakN: weak.length, resist };
|
||||
});
|
||||
|
||||
// A "weak spot": 2+ members weak, or someone weak and nobody resists it.
|
||||
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 weakSpots = threats.length
|
||||
? el(
|
||||
'div',
|
||||
{ class: 'weakspots' },
|
||||
...threats.map((a) =>
|
||||
el(
|
||||
'div',
|
||||
{ class: `weakspot${a.resist === 0 ? ' is-nores' : ''}` },
|
||||
TypeChip(a.type),
|
||||
el('span', { class: 'weakspot__count' }, `${a.weakN} weak`),
|
||||
a.resist === 0
|
||||
? el('span', { class: 'weakspot__flag' }, '0 resist')
|
||||
: el('span', { class: 'weakspot__res' }, `${a.resist} resist`),
|
||||
el(
|
||||
'span',
|
||||
{ class: 'weakspot__mem' },
|
||||
...a.weak.map(({ sp, m }) =>
|
||||
el(
|
||||
'a',
|
||||
{
|
||||
class: 'weakspot__chip',
|
||||
href: `#/pokemon/${sp.id}`,
|
||||
title: `${prettify(sp.name)} takes ${m}×`,
|
||||
},
|
||||
Sprite(sp.id, { style, size: 26, alt: sp.name }),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: el('p', { class: 'cov__ok' }, '✓ Nothing hits two or more of your team super-effectively.');
|
||||
|
||||
// Defensive grid: a diverging bar (resists ◀ | ▶ weaknesses) makes the
|
||||
// rows to worry about jump out; threat rows get a tinted background.
|
||||
const head = el(
|
||||
'div',
|
||||
{ class: 'cov__row cov__row--head' },
|
||||
el('span', {}, ''),
|
||||
...mem.map((sp) => el('span', { class: 'cov__mem' }, Sprite(sp.id, { size: 34, alt: sp.name }))),
|
||||
el('span', { class: 'cov__sum' }, 'Weak'),
|
||||
el('span', { class: 'cov__type' }, ''),
|
||||
...mem.map((sp) =>
|
||||
el(
|
||||
'a',
|
||||
{ class: 'cov__mem', href: `#/pokemon/${sp.id}`, title: prettify(sp.name) },
|
||||
Sprite(sp.id, { style, size: 34, alt: sp.name }),
|
||||
),
|
||||
),
|
||||
el('span', { class: 'cov__bal cov__bal--head' }, 'resist · weak'),
|
||||
);
|
||||
const rows = TYPES.map((t) => {
|
||||
let weak = 0;
|
||||
const rows = GEN_TYPES.map((t) => {
|
||||
const a = analysis.find((x) => x.type === t);
|
||||
const cells = vectors.map((v) => {
|
||||
const m = v[t] ?? 1;
|
||||
if (m >= 2) weak += 1;
|
||||
return el('span', { class: `cov__cell ${multClass(m)}` }, multText(m));
|
||||
});
|
||||
const bal = el(
|
||||
'span',
|
||||
{ class: 'cov__bal' },
|
||||
el(
|
||||
'span',
|
||||
{ class: 'cov__bal-side cov__bal-side--l' },
|
||||
el('span', { class: 'cov__bal-res', style: `width:${Math.min(a.resist, 6) * 10}px` }),
|
||||
),
|
||||
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 || ''),
|
||||
),
|
||||
);
|
||||
return el(
|
||||
'div',
|
||||
{ class: 'cov__row' },
|
||||
{ class: `cov__row${a.weakN >= 2 ? ' cov__row--threat' : ''}` },
|
||||
el('span', { class: 'cov__type' }, TypeChip(t)),
|
||||
...cells,
|
||||
el('span', { class: `cov__sum${weak >= 2 ? ' is-bad' : ''}` }, weak || ''),
|
||||
bal,
|
||||
);
|
||||
});
|
||||
|
||||
// Offensive coverage gaps: defending types no member hits super-effectively
|
||||
const gaps = TYPES.filter((def) => {
|
||||
if (gen < 6 && def === 'fairy') return false;
|
||||
return !mem.some((sp) => sp.types.some((atk) => multiplier(atk, [def], gen) >= 2));
|
||||
});
|
||||
const gaps = GEN_TYPES.filter(
|
||||
(def) => !mem.some((sp, i) => memTypes[i].some((atk) => multiplier(atk, [def], gen) >= 2)),
|
||||
);
|
||||
|
||||
// Quick team stats
|
||||
const avgBst = Math.round(mem.reduce((s, m) => s + (m.bst || 0), 0) / mem.length);
|
||||
@ -190,7 +277,17 @@ export async function TeamView() {
|
||||
return el(
|
||||
'div',
|
||||
{},
|
||||
el('h2', { class: 'ppanel__sub' }, 'Type coverage — how each type hits your team'),
|
||||
el('h2', { class: 'ppanel__sub' }, `Weak spots · ${gameLabel} (Gen ${gen})`),
|
||||
weakSpots,
|
||||
unresisted.length
|
||||
? el(
|
||||
'p',
|
||||
{ class: 'cov__unres' },
|
||||
el('span', { class: 'cov__unres-k' }, 'No resist to: '),
|
||||
el('span', { class: 'matchups__types' }, ...unresisted.map(TypeChip)),
|
||||
)
|
||||
: null,
|
||||
el('h2', { class: 'ppanel__sub' }, 'Full matchup grid'),
|
||||
el('div', { class: 'cov' }, head, ...rows),
|
||||
el('h2', { class: 'ppanel__sub' }, 'Offensive gaps (no super-effective STAB)'),
|
||||
gaps.length
|
||||
@ -213,6 +310,8 @@ export async function TeamView() {
|
||||
}
|
||||
|
||||
function compare(mem) {
|
||||
const vg = snap.versionGroupByKey.get(settings.get().versionGroup);
|
||||
const gen = vg ? vg.generation : 9;
|
||||
const maxOf = (key) => Math.max(...mem.map((m) => m.stats?.[key] ?? 0));
|
||||
const row = (label, cells, cls = '') =>
|
||||
el('div', { class: `cmp__row ${cls}` }, el('span', { class: 'cmp__label' }, label), ...cells);
|
||||
@ -234,7 +333,7 @@ export async function TeamView() {
|
||||
),
|
||||
row(
|
||||
'Type',
|
||||
mem.map((sp) => el('span', { class: 'cmp__cell' }, ...sp.types.map(TypeChip))),
|
||||
mem.map((sp) => el('span', { class: 'cmp__cell' }, ...typesForGen(sp, gen).map(TypeChip))),
|
||||
),
|
||||
...STAT_ROWS.map(([key, label]) => {
|
||||
const mx = maxOf(key);
|
||||
@ -270,10 +369,12 @@ export async function TeamView() {
|
||||
|
||||
render();
|
||||
const off = team.subscribe(render);
|
||||
const offStyle = settings.subscribe(renderLineup);
|
||||
// Re-render on any settings change: sprite style feeds the lineup, and the
|
||||
// selected game drives the coverage generation + era typings.
|
||||
const offSettings = settings.subscribe(render);
|
||||
onTeardown(view, () => {
|
||||
off();
|
||||
offStyle();
|
||||
offSettings();
|
||||
});
|
||||
return view;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user