pogo-data-generator
Version:
Pokemon GO project data generator
511 lines (510 loc) • 21.2 kB
JavaScript
"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;