LoLStats: Champion Tier List pro Game Mode (ARAM, Arena, URF, etc.)
Neue op.gg Champion API (lol-api-champion.op.gg) fuer Tier-Daten. 30-Min In-Memory Cache pro Mode+Region. Neuer Bereich unterhalb der Match History mit Mode-Tabs, Champion-Grid, Tier-Badges und Filter. Arena-Mode zeigt Avg Placement statt KDA. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9093f12259
commit
905f156d47
5 changed files with 397 additions and 3 deletions
|
|
@ -2,7 +2,7 @@ import type express from 'express';
|
|||
import type { Plugin, PluginContext } from '../../core/plugin.js';
|
||||
import { sseBroadcast } from '../../core/sse.js';
|
||||
import { getState, setState } from '../../core/persistence.js';
|
||||
import { getProfile, getMatches, getMatchDetail, renewSummoner, REGIONS } from './opgg-api.js';
|
||||
import { getProfile, getMatches, getMatchDetail, renewSummoner, REGIONS, GAME_MODES, getChampionTierList } from './opgg-api.js';
|
||||
import type { RecentSearch } from './types.js';
|
||||
|
||||
// ── Recent searches ──
|
||||
|
|
@ -120,6 +120,26 @@ const lolstatsPlugin: Plugin = {
|
|||
}
|
||||
});
|
||||
|
||||
// ── Game Modes ──
|
||||
app.get('/api/lolstats/modes', (_req, res) => {
|
||||
res.json(GAME_MODES);
|
||||
});
|
||||
|
||||
// ── Champion Tier List by Mode ──
|
||||
app.get('/api/lolstats/tierlist', async (req, res) => {
|
||||
const { mode, region } = req.query as Record<string, string>;
|
||||
if (!mode || !region) {
|
||||
return res.status(400).json({ error: 'mode and region required' });
|
||||
}
|
||||
try {
|
||||
const tierList = await getChampionTierList(mode, region);
|
||||
res.json(tierList);
|
||||
} catch (e: any) {
|
||||
console.error('[LoLStats] Tier list error:', e.message);
|
||||
res.status(502).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ── Recent Searches ──
|
||||
app.get('/api/lolstats/recent', (_req, res) => {
|
||||
res.json(getRecent());
|
||||
|
|
|
|||
|
|
@ -7,10 +7,12 @@
|
|||
|
||||
import type {
|
||||
RegionInfo, SummonerProfile, MatchEntry, MatchParticipant,
|
||||
MatchParticipantStats, TeamStat,
|
||||
MatchParticipantStats, TeamStat, GameModeInfo, ChampionTierEntry,
|
||||
ChampionTierList,
|
||||
} from './types.js';
|
||||
|
||||
const OPGG_API = 'https://lol-api-summoner.op.gg/api';
|
||||
const CHAMPION_API = 'https://lol-api-champion.op.gg/api';
|
||||
const MCP_URL = 'https://mcp-api.op.gg/mcp';
|
||||
let rpcId = 1;
|
||||
|
||||
|
|
@ -690,3 +692,87 @@ async function getMatchDetailMCP(
|
|||
const parsed = parseClassNotation(raw);
|
||||
return parsed?.game_detail ?? parsed?.data?.game_detail ?? null;
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// Champion Tier List per Game Mode
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
export const GAME_MODES: GameModeInfo[] = [
|
||||
{ key: 'aram', label: 'ARAM' },
|
||||
{ key: 'arena', label: 'Arena' },
|
||||
{ key: 'urf', label: 'URF' },
|
||||
{ key: 'nexus_blitz', label: 'Nexus Blitz' },
|
||||
{ key: 'ranked', label: 'Ranked' },
|
||||
{ key: 'flex', label: 'Flex' },
|
||||
];
|
||||
|
||||
const tierListCache = new Map<string, { data: ChampionTierList; expires: number }>();
|
||||
const TIER_CACHE_TTL = 30 * 60 * 1000; // 30 minutes
|
||||
|
||||
export async function getChampionTierList(
|
||||
mode: string, region: string,
|
||||
): Promise<ChampionTierList> {
|
||||
await loadChampionMap();
|
||||
const regionLower = region.toLowerCase();
|
||||
const cacheKey = `${regionLower}:${mode}`;
|
||||
|
||||
const cached = tierListCache.get(cacheKey);
|
||||
if (cached && cached.expires > Date.now()) {
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
const url = `${CHAMPION_API}/${regionLower}/champions/${mode}?hl=en_US`;
|
||||
console.log(`[LoLStats] Fetching champion tier list: ${mode} @ ${region}`);
|
||||
|
||||
const res = await fetch(url, { headers: { 'Accept': 'application/json' } });
|
||||
if (!res.ok) {
|
||||
throw new Error(`Champion tier list HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
const json = await res.json() as any;
|
||||
const data = json?.data;
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
throw new Error('Invalid champion tier list response');
|
||||
}
|
||||
|
||||
const isArena = mode === 'arena';
|
||||
|
||||
const champions: ChampionTierEntry[] = data.map((c: any) => {
|
||||
const stats = c.average_stats ?? {};
|
||||
const tierData = stats.tier_data ?? {};
|
||||
const entry: ChampionTierEntry = {
|
||||
champion_id: c.id,
|
||||
champion_name: champName(c.id),
|
||||
tier: tierData.tier ?? stats.tier ?? 5,
|
||||
rank: tierData.rank ?? stats.rank ?? 999,
|
||||
win_rate: isArena
|
||||
? (stats.play > 0 ? stats.win / stats.play : null)
|
||||
: (stats.win_rate ?? null),
|
||||
pick_rate: stats.pick_rate ?? 0,
|
||||
ban_rate: stats.ban_rate ?? null,
|
||||
kda: stats.kda ?? null,
|
||||
play: stats.play ?? 0,
|
||||
};
|
||||
|
||||
if (isArena) {
|
||||
entry.average_placement = stats.play > 0 ? stats.total_place / stats.play : undefined;
|
||||
entry.first_place_rate = stats.play > 0 ? stats.first_place / stats.play : undefined;
|
||||
}
|
||||
|
||||
return entry;
|
||||
});
|
||||
|
||||
champions.sort((a, b) => a.rank - b.rank);
|
||||
|
||||
const result: ChampionTierList = {
|
||||
mode,
|
||||
region: regionLower,
|
||||
champions,
|
||||
fetched_at: Date.now(),
|
||||
};
|
||||
|
||||
tierListCache.set(cacheKey, { data: result, expires: Date.now() + TIER_CACHE_TTL });
|
||||
console.log(`[LoLStats] Cached ${champions.length} champions for ${mode}@${region}`);
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,6 +132,34 @@ export interface MatchDetailResponse {
|
|||
match: MatchEntry;
|
||||
}
|
||||
|
||||
// ── Champion Tier List (Game Modes) ──
|
||||
|
||||
export interface GameModeInfo {
|
||||
key: string; // "aram", "arena", etc.
|
||||
label: string; // "ARAM", "Arena", etc.
|
||||
}
|
||||
|
||||
export interface ChampionTierEntry {
|
||||
champion_id: number;
|
||||
champion_name: string;
|
||||
tier: number; // 0-5 (0=OP/S-tier, 5=niedrigste)
|
||||
rank: number; // 1=bester
|
||||
win_rate: number | null;
|
||||
pick_rate: number;
|
||||
ban_rate: number | null;
|
||||
kda: number | null;
|
||||
play: number;
|
||||
average_placement?: number; // Arena-spezifisch
|
||||
first_place_rate?: number; // Arena-spezifisch
|
||||
}
|
||||
|
||||
export interface ChampionTierList {
|
||||
mode: string;
|
||||
region: string;
|
||||
champions: ChampionTierEntry[];
|
||||
fetched_at: number;
|
||||
}
|
||||
|
||||
// ── Recent search persistence ──
|
||||
|
||||
export interface RecentSearch {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue