import { normName, unique } from './utils.js';
type AnyObj = Record<string, any>;
const API = 'https://pokeapi.co/api/v2';
class Cache<T=any> extends Map<string, Promise<T>> {}
export class PokeAPI {
private pokemonCache = new Cache<AnyObj>();
private speciesCache = new Cache<AnyObj>();
private moveCache = new Cache<AnyObj>();
private typeCache = new Cache<AnyObj>();
private listCache = new Cache<{results:{name:string,url:string}[]}>();
private async getJSON<T=any>(url: string): Promise<T> {
const res = await fetch(url, { headers: { 'user-agent': 'pokemon-mcp/1.0' } });
if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
return await res.json() as T;
}
async list(resource: 'pokemon'|'move'): Promise<{name:string,url:string}[]> {
const key = `list:${resource}`;
if (!this.listCache.has(key)) {
this.listCache.set(key, this.getJSON(`${API}/${resource}?limit=20000`));
}
const data = await this.listCache.get(key)!;
return data.results;
}
async listNames(resource: 'pokemon'|'move'): Promise<string[]> {
const list = await this.list(resource);
return list.map(x => x.name);
}
async getPokemon(name: string): Promise<AnyObj> {
const n = normName(name);
if (!this.pokemonCache.has(n)) this.pokemonCache.set(n, this.getJSON(`${API}/pokemon/${n}`));
return this.pokemonCache.get(n)!;
}
async getSpecies(name: string): Promise<AnyObj> {
const n = normName(name);
if (!this.speciesCache.has(n)) this.speciesCache.set(n, this.getJSON(`${API}/pokemon-species/${n}`));
return this.speciesCache.get(n)!;
}
async getEvolutionChainBySpeciesName(name: string): Promise<AnyObj> {
const sp = await this.getSpecies(name);
const url = sp.evolution_chain.url as string;
return this.getJSON(url);
}
async getMove(name: string): Promise<AnyObj> {
const n = normName(name);
if (!this.moveCache.has(n)) this.moveCache.set(n, this.getJSON(`${API}/move/${n}`));
return this.moveCache.get(n)!;
}
async getType(name: string): Promise<AnyObj> {
const n = normName(name);
if (!this.typeCache.has(n)) this.typeCache.set(n, this.getJSON(`${API}/type/${n}`));
return this.typeCache.get(n)!;
}
// Compute offensive multiplier using PokeAPI damage relations
async typeMultiplier(attacking: string, defending: string[]): Promise<number> {
const atk = await this.getType(attacking);
const rel = atk.damage_relations;
const dbl = rel.double_damage_to.map((t:AnyObj)=>t.name);
const half = rel.half_damage_to.map((t:AnyObj)=>t.name);
const zero = rel.no_damage_to.map((t:AnyObj)=>t.name);
let mult = 1;
for (const d of defending) {
if (zero.includes(normName(d))) { mult *= 0; continue; }
if (dbl.includes(normName(d))) mult *= 2;
else if (half.includes(normName(d))) mult *= 0.5;
}
return mult;
}
// Learnset helpers
methodAlias(m: string): string {
const a = m.toLowerCase();
if (a.includes('level')) return 'level';
if (a.includes('machine')) return 'machine';
if (a.includes('tutor')) return 'tutor';
if (a.includes('egg')) return 'egg';
return a;
}
validVersionGroup(g?: string): string|undefined {
if (!g) return undefined;
const ok = ['red-blue','yellow','gold-silver','crystal','ruby-sapphire','firered-leafgreen','emerald'];
const x = normName(g);
return ok.includes(x) ? x : undefined;
}
async learnset(name: string, version_group?: string) {
const p = await this.getPokemon(name);
const vg = this.validVersionGroup(version_group);
const rows = p.moves as AnyObj[];
const out: { move: string, methods: {version_group:string, method:string, level:number}[] }[] = [];
for (const r of rows) {
const methods = (r.version_group_details as AnyObj[])
.filter(d => !vg || d.version_group.name === vg)
.map(d => ({
version_group: d.version_group.name,
method: this.methodAlias(d.move_learn_method.name),
level: d.level_learned_at
}));
if (methods.length) out.push({ move: r.move.name, methods });
}
return out;
}
async pokemonCore(name: string) {
const p = await this.getPokemon(name);
return {
name: p.name,
types: (p.types as AnyObj[]).sort((a,b)=>a.slot-b.slot).map(t => t.type.name),
base_stats: Object.fromEntries((p.stats as AnyObj[]).map(s => [s.stat.name.replace('special-attack','spa').replace('special-defense','spd').replace('speed','spe').replace('attack','atk').replace('defense','def').replace('hp','hp'), s.base_stat])),
abilities: (p.abilities as AnyObj[]).map(a => a.ability.name)
};
}
parseEvolutionChain(chain: AnyObj) {
const edges: {from:string,to:string,conditions:AnyObj}[] = [];
function walk(node: AnyObj) {
const from = node.species.name;
for (const evo of node.evolves_to as AnyObj[]) {
const to = evo.species.name;
const conds = (evo.evolution_details?.[0]) || {};
edges.push({ from, to, conditions: conds });
walk(evo);
}
}
walk(chain.chain);
return edges;
}
async searchNames(query: string) {
const q = normName(query);
const poke = await this.listNames('pokemon');
const moves = await this.listNames('move');
const filt = (arr:string[]) => unique(arr.filter(n => n.includes(q))).slice(0, 50);
return { pokemon: filt(poke), moves: filt(moves) };
}
}
export const pokeapi = new PokeAPI();