UNPKG

pogo-data-generator

Version:
511 lines (510 loc) 21.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.sanitizePokeApiBaseStatsForCache = void 0; const pogo_protos_1 = require("@na-ji/pogo-protos"); const tempEvolutions_1 = require("../utils/tempEvolutions"); const Masterfile_1 = __importDefault(require("./Masterfile")); const excludedFallbackChargedMoves = new Set([ pogo_protos_1.Rpc.HoloPokemonMove.FRUSTRATION, pogo_protos_1.Rpc.HoloPokemonMove.RETURN, ]); const sanitizePokeApiBaseStatsForCache = (baseStats) => Object.fromEntries(Object.entries(baseStats).map(([id, entry]) => { if (!entry?.chargedMoves?.length) { return [id, entry]; } const chargedMoves = entry.chargedMoves.filter((move) => !excludedFallbackChargedMoves.has(move)); if (chargedMoves.length === entry.chargedMoves.length) { return [id, entry]; } return [ id, { ...entry, chargedMoves, ...(chargedMoves.length === 0 ? { _hiddenOnlyChargedMoves: true } : {}), }, ]; })); exports.sanitizePokeApiBaseStatsForCache = sanitizePokeApiBaseStatsForCache; class PokeApi extends Masterfile_1.default { baseStats; tempEvos; types; maxPokemon; inconsistentStats; moveReference; pokemonStatsCache; speciesCache; inheritedMoveParentOverrides; apiBaseUrl; constructor(baseUrl) { super(); this.apiBaseUrl = (baseUrl || 'https://pokeapi.co/api/v2').replace(/\/$/, ''); this.baseStats = {}; this.tempEvos = {}; this.types = {}; this.pokemonStatsCache = {}; this.speciesCache = {}; this.inheritedMoveParentOverrides = { 'basculegion-female': 'basculin-white-striped', 'basculegion-male': 'basculin-white-striped', }; this.maxPokemon = 1008; this.inconsistentStats = { 24: { attack: 167, }, 51: { attack: 167, defense: 134, }, 83: { attack: 124, }, 85: { attack: 218, defense: 140, }, 101: { attack: 173, defense: 173, }, 103: { defense: 149, }, 164: { attack: 145, }, 168: { defense: 124, }, 176: { attack: 139, }, 211: { defense: 138, }, 219: { attack: 139, stamina: 137, }, 222: { defense: 156, stamina: 146, }, 226: { attack: 148, stamina: 163, }, 227: { attack: 148, stamina: 163, }, 241: { attack: 157, }, 292: { stamina: 1, }, 809: { stamina: 264, }, }; } set moves(parsed) { this.moveReference = parsed; } isKnownMove(move) { return !!move && !!this.moveReference?.[move]; } hasExactMoves(moves, expected) { return (Array.isArray(moves) && moves.length === expected.length && moves.every((move, index) => move === expected[index])); } shouldFetchPlaceholderMoves(pokemon) { return (this.hasExactMoves(pokemon?.quickMoves, [pogo_protos_1.Rpc.HoloPokemonMove.SPLASH_FAST]) && this.hasExactMoves(pokemon?.chargedMoves, [pogo_protos_1.Rpc.HoloPokemonMove.STRUGGLE])); } buildUrl(path) { return `${this.apiBaseUrl}/${path.replace(/^\//, '')}`; } normalizeUrl(url) { const match = url?.match(/\/api\/v2\/(.+)/); if (match?.[1]) { return this.buildUrl(match[1]); } return url; } buildStatMap(stats) { const baseStats = {}; stats.forEach((stat) => { baseStats[stat.stat.name] = stat.base_stat; }); return baseStats; } typeNameToTypeId(typeName) { return pogo_protos_1.Rpc.HoloPokemonType[`POKEMON_TYPE_${typeName.toUpperCase()}`]; } mapTypeIds(types) { return types .map((type) => this.typeNameToTypeId(type.type.name)) .sort((a, b) => a - b); } mapNamedTypeIds(types) { return types .map((type) => this.typeNameToTypeId(type.name)) .sort((a, b) => a - b); } resolveStructId(struct) { if (!struct) { return undefined; } const protoId = pogo_protos_1.Rpc.HoloPokemonId[struct.name.toUpperCase().replace(/-/g, '_')]; if (protoId) { return protoId; } const idFromUrl = Number.parseInt(struct.url.split('/').at(-2) || '', 10); return Number.isFinite(idFromUrl) ? idFromUrl : undefined; } async fetchPokemonStats(id) { const cacheKey = `${id}`; if (!this.pokemonStatsCache[cacheKey]) { this.pokemonStatsCache[cacheKey] = this.fetch(this.buildUrl(`pokemon/${id}`)).catch((error) => { delete this.pokemonStatsCache[cacheKey]; throw error; }); } const statsData = await this.pokemonStatsCache[cacheKey]; this.pokemonStatsCache[cacheKey] = statsData; return statsData; } async fetchSpecies(id) { const cacheKey = `${id}`; if (!this.speciesCache[cacheKey]) { this.speciesCache[cacheKey] = this.fetch(this.buildUrl(`pokemon-species/${id}`)).catch((error) => { delete this.speciesCache[cacheKey]; throw error; }); } const speciesData = await this.speciesCache[cacheKey]; this.speciesCache[cacheKey] = speciesData; return speciesData; } async fetchSpeciesForPokemon(id, statsData) { const speciesId = this.resolveStructId(statsData.species); if (speciesId !== undefined) { return this.fetchSpecies(speciesId); } if (statsData.species?.name) { return this.fetchSpecies(statsData.species.name); } return this.fetchSpecies(id); } mapPokeApiMoves(statsData) { return { quickMoves: statsData.moves .map((move) => pogo_protos_1.Rpc.HoloPokemonMove[`${move.move.name .toUpperCase() .replace(/-/g, '_')}_FAST`]) .filter((move) => this.isKnownMove(move)), chargedMoves: statsData.moves .map((move) => pogo_protos_1.Rpc.HoloPokemonMove[move.move.name.toUpperCase().replace(/-/g, '_')]) .filter((move) => this.isKnownMove(move)), }; } mergeMoveLists(...moveLists) { return Array.from(new Set(moveLists.flat())).sort((a, b) => a - b); } resolveInheritedParentIdentifier(pokemonName, speciesData) { return (this.inheritedMoveParentOverrides[pokemonName] || this.resolveStructId(speciesData.evolves_from_species)); } async getInheritedMoves(id, seen = new Set()) { const cacheKey = `${id}`; if (seen.has(cacheKey)) { return { quickMoves: [], chargedMoves: [] }; } seen.add(cacheKey); try { const statsData = await this.fetchPokemonStats(id); const currentMoves = this.mapPokeApiMoves(statsData); const speciesData = await this.fetchSpeciesForPokemon(id, statsData); const previousId = this.resolveInheritedParentIdentifier(statsData.name, speciesData); if (!previousId) { return { quickMoves: this.mergeMoveLists(currentMoves.quickMoves), chargedMoves: this.mergeMoveLists(currentMoves.chargedMoves), }; } const previousMoves = await this.getInheritedMoves(previousId, seen); return { quickMoves: this.mergeMoveLists(currentMoves.quickMoves, previousMoves.quickMoves), chargedMoves: this.mergeMoveLists(currentMoves.chargedMoves, previousMoves.chargedMoves), }; } finally { seen.delete(cacheKey); } } calculatePogoStats(baseStats, nerf = false) { return { attack: PokeApi.attack(baseStats.attack, baseStats['special-attack'], baseStats.speed, nerf), defense: PokeApi.defense(baseStats.defense, baseStats['special-defense'], baseStats.speed, nerf), stamina: PokeApi.stamina(baseStats.hp, nerf), }; } static attack(normal, special, speed, nerf = false) { return Math.round(Math.round(2 * (0.875 * Math.max(normal, special) + 0.125 * Math.min(normal, special))) * (1 + (speed - 75) / 500) * (nerf ? 0.91 : 1)); } static defense(normal, special, speed, nerf = false) { return Math.round(Math.round(2 * (0.625 * Math.max(normal, special) + 0.375 * Math.min(normal, special))) * (1 + (speed - 75) / 500) * (nerf ? 0.91 : 1)); } static stamina(hp, nerf = false) { return nerf ? Math.round((1.75 * hp + 50) * 0.91) : Math.floor(1.75 * hp + 50); } cp(atk, def, sta, cpm) { return Math.floor(((atk + 15) * (def + 15) ** 0.5 * (sta + 15) ** 0.5 * cpm ** 2) / 10); } megaLookup(id, type) { switch (true) { case id.endsWith('mega-y'): return 3; case id.endsWith('mega-x'): return 2; case id.endsWith('mega-z'): return 5; case id.endsWith('mega'): return 1; } return this.capitalize(type); } async setMaxPokemonId() { const { count } = await this.fetch(this.buildUrl('pokemon-species/?limit=1&offset=0')); this.maxPokemon = +count; return +count; } async baseStatsApi(parsedPokemon, pokeApiIds) { await Promise.all(Object.keys(parsedPokemon).map(async (id) => { if (!parsedPokemon[id].attack || !parsedPokemon[id].defense || !parsedPokemon[id].stamina || parsedPokemon[id].types.length === 0 || pokeApiIds?.includes(+id) || this.shouldFetchPlaceholderMoves(parsedPokemon[id])) { await this.pokemonApi(id, false); } })); } async extraPokemon(parsedPokemon) { const extraPokemon = []; for (let i = 1; i <= this.maxPokemon; i++) { if (!parsedPokemon[i]) { extraPokemon.push(i); } } await Promise.all(extraPokemon.map((id) => this.pokemonApi(id, true))); } async pokemonApi(id, unreleased = false) { try { const statsData = await this.fetchPokemonStats(id); const inheritedMoves = await this.getInheritedMoves(id); const baseStats = this.buildStatMap(statsData.stats); const initial = this.calculatePogoStats(baseStats); const cp = this.cp(initial.attack, initial.defense, initial.stamina, 0.79030001); const nerfCheck = cp > 4000 ? this.calculatePogoStats(baseStats, true) : initial; this.baseStats[id] = { pokemonName: this.capitalize(statsData.name), quickMoves: inheritedMoves.quickMoves, chargedMoves: inheritedMoves.chargedMoves, attack: this.inconsistentStats[id] ? this.inconsistentStats[id].attack || nerfCheck.attack : nerfCheck.attack, defense: this.inconsistentStats[id] ? this.inconsistentStats[id].defense || nerfCheck.defense : nerfCheck.defense, stamina: this.inconsistentStats[id] ? this.inconsistentStats[id].stamina || nerfCheck.stamina : nerfCheck.stamina, types: this.mapTypeIds(statsData.types), ...(unreleased ? { unreleased: true } : {}), }; } catch (e) { console.warn(e, `Failed to parse PokeApi Stats for #${id}`); } } async evoApi(evolvedPokemon, parsedPokemon) { await Promise.all(Object.keys(parsedPokemon).map(async (id) => { try { if (!evolvedPokemon.has(+id)) { const evoData = await this.fetchSpecies(id); if (this.baseStats[id]) { this.baseStats[id].legendary = parsedPokemon[id]?.legendary ?? evoData.is_legendary; this.baseStats[id].mythic = parsedPokemon[id]?.mythic ?? evoData.is_mythical; } if (evoData.evolves_from_species) { const prevEvoId = this.resolveStructId(evoData.evolves_from_species); if (prevEvoId) { if (!this.baseStats[prevEvoId]) { this.baseStats[prevEvoId] = {}; } if (!this.baseStats[prevEvoId].evolutions) { this.baseStats[prevEvoId].evolutions = []; } this.baseStats[prevEvoId].evolutions.push({ evoId: +id, formId: parsedPokemon[id]?.defaultFormId || +Object.keys(parsedPokemon[id]?.forms || {})[0] || 0, }); this.baseStats[prevEvoId].evolutions.sort((a, b) => a.evoId - b.evoId); evolvedPokemon.add(+id); } else { console.warn('Unable to find proto ID for', evoData.evolves_from_species.name .toUpperCase() .replace(/-/g, '_')); } } } } catch (e) { console.warn(e, `Failed to parse PokeApi Evolutions for #${id}`); } })); await Promise.all(Object.keys(this.baseStats).map(async (id) => { try { const evoData = await this.fetchSpecies(id); this.baseStats[id].legendary = parsedPokemon[id]?.legendary ?? evoData.is_legendary; this.baseStats[id].mythic = parsedPokemon[id]?.mythic ?? evoData.is_mythical; } catch (e) { console.warn(e, `Failed to apply PokeApi species flags for #${id}`); } })); } async tempEvoApi(parsedPokemon) { const discoveredMega = (await this.fetch(this.buildUrl('pokemon?limit=100000&offset=0')))?.results ?.map((pokemon) => pokemon.name) ?.filter((name) => /-mega(?:-[xyz])?$/.test(name)) || []; const type = 'mega'; this.tempEvos[type] = {}; const megaIds = Array.from(new Set(discoveredMega)); await Promise.all(megaIds.map(async (id) => { try { const statsData = await this.fetch(this.buildUrl(`pokemon/${id}`)); if (!statsData) return; const pokemonId = (statsData.species?.name ? pogo_protos_1.Rpc.HoloPokemonId[statsData.species.name .toUpperCase() .replace(/-/g, '_')] : undefined) || Number.parseInt(statsData.species?.url?.split('/').at(-2) || '', 10); if (!pokemonId) { console.warn('Unable to resolve Pokemon ID for temp evo', id); return; } const baseStats = this.buildStatMap(statsData.stats); const types = this.mapTypeIds(statsData.types); const computedStats = this.calculatePogoStats(baseStats); const baseTypes = parsedPokemon[pokemonId]?.types || this.baseStats[pokemonId]?.types; const newTheoretical = { tempEvoId: this.megaLookup(id, type), attack: computedStats.attack, defense: computedStats.defense, stamina: computedStats.stamina, types: baseTypes && this.compare(types, baseTypes) ? undefined : types, unreleased: true, }; const alreadyExistsInGame = parsedPokemon[pokemonId]?.tempEvolutions?.some((temp) => temp.tempEvoId === newTheoretical.tempEvoId); if (alreadyExistsInGame) return; if (!this.tempEvos[type][pokemonId]) { this.tempEvos[type][pokemonId] = {}; } if (!this.tempEvos[type][pokemonId].tempEvolutions) { this.tempEvos[type][pokemonId].tempEvolutions = []; } const existingTempEvolution = this.tempEvos[type][pokemonId].tempEvolutions.find((temp) => temp.tempEvoId === newTheoretical.tempEvoId); if (existingTempEvolution) { const typesEqual = (!existingTempEvolution.types && !newTheoretical.types) || (Array.isArray(existingTempEvolution.types) && Array.isArray(newTheoretical.types) && this.compare(existingTempEvolution.types, newTheoretical.types)); const isExactDuplicate = existingTempEvolution.attack === newTheoretical.attack && existingTempEvolution.defense === newTheoretical.defense && existingTempEvolution.stamina === newTheoretical.stamina && existingTempEvolution.unreleased === newTheoretical.unreleased && typesEqual; if (isExactDuplicate) return; if (!existingTempEvolution.types && newTheoretical.types) { existingTempEvolution.types = newTheoretical.types; } return; } this.tempEvos[type][pokemonId].tempEvolutions = (0, tempEvolutions_1.sortTempEvolutions)([ ...this.tempEvos[type][pokemonId].tempEvolutions, newTheoretical, ]); } catch (e) { console.warn(e, `Failed to parse PokeApi ${type} Evos for ${id}`); } })); } async typesApi() { await Promise.all(Object.entries(pogo_protos_1.Rpc.HoloPokemonType).map(async ([type, id]) => { try { const { damage_relations: { double_damage_from, double_damage_to, half_damage_from, half_damage_to, no_damage_from, no_damage_to, }, } = id ? await this.fetch(this.buildUrl(`type/${type.substring(13).toLowerCase()}`)) : { damage_relations: {} }; this.types[id] = { strengths: id ? this.mapNamedTypeIds(double_damage_to) : [], weaknesses: id ? this.mapNamedTypeIds(double_damage_from) : [], veryWeakAgainst: id ? this.mapNamedTypeIds(no_damage_to) : [], immunes: id ? this.mapNamedTypeIds(no_damage_from) : [], weakAgainst: id ? this.mapNamedTypeIds(half_damage_to) : [], resistances: id ? this.mapNamedTypeIds(half_damage_from) : [], }; } catch (e) { console.warn(`Unable to fetch ${type}`, e); } })); } async getGenerations() { const generations = await this.fetch(this.buildUrl('generation')); const results = await Promise.all(generations.results.map(async (gen, index) => { const { main_region, pokemon_species, } = await this.fetch(this.normalizeUrl(gen.url)); const name = this.capitalize(main_region.name); const pokemonIds = pokemon_species.map((pokemon) => +pokemon.url.split('/').at(-2)); const min = Math.min(...pokemonIds); const max = Math.max(...pokemonIds); return { id: index + 1, name, range: [min, max] }; })); return Object.fromEntries(results.map(({ id, ...rest }) => [id, rest])); } } exports.default = PokeApi;