import * as fs from 'fs'; import PokemonDescription from 'pokemongo-json-pokedex/output/locales/en-US/pokemon.json'; import Pokemon from 'pokemongo-json-pokedex/output/pokemon.json'; import { LevelMultipliers } from 'app/models/LevelMultipliers'; import { Grade, IBaseStatsRank, ILeaguePokemon, IMaxStats, IPokemon, IStats, League, PokemonId, Type } from 'app/models/Pokemon'; type ICpAndTotalFound = Record>; interface IStatsDistribution { great : ICpAndTotalFound; ultra : ICpAndTotalFound; } interface IMaxCpByLeague { great : number; ultra : number; } interface ICalculateRelativeStats { id : string; value : number; } const outPath = './dist/db/'; const maxCpByLeague : IMaxCpByLeague = { great: 1500, ultra: 2500 }; const maxPossibleStats : IMaxStats = { baseStamina: 0, baseAttack: 0, baseDefense: 0, level: 40, }; const pokemonOrderById : Record = {}; const pokemonBaseStamina : Array = []; const pokemonBaseAttack : Array = []; const pokemonBaseDefense : Array = []; const getClosestCpMultiplierIndex = (value : number) => { let i; for (i = 0; i < LevelMultipliers.length; i++) { if (value < LevelMultipliers[i]) { break; } } return Math.max(i - 1, 0); }; const familyOrder : Array = []; const familyEvolutionOrder : Record> = {}; const familyEncountered : Record> = {}; const parseNameAndForm = (monId : string, monName : string) => { if (monId.indexOf('_ALOLA') > -1 || monId.indexOf('CASTFORM_') > -1 || monId.indexOf('DEOXYS_') > -1 || monId.indexOf('WORMADAM_') > -1 || monId.indexOf('BURMY_') > -1 || monId.indexOf('CHERRIM_') > -1 || monId.indexOf('SHELLOS_') > -1 || monId.indexOf('GASTRODON_') > -1 || monId.indexOf('ROTOM_') > -1 || monId.indexOf('GIRATINA_') > -1 || monId.indexOf('SHAYMIN_') > -1 || monId.indexOf('ARCEUS_') > -1 ) { const formTokenIndex = monId.indexOf('_'); return { name: monName.substr(0, formTokenIndex), form: monId.substr(formTokenIndex + 1), }; } return { name: monName, form: null, }; }; Pokemon.forEach((mon) => { maxPossibleStats.baseStamina = Math.max(mon.stats.baseStamina, maxPossibleStats.baseStamina); maxPossibleStats.baseAttack = Math.max(mon.stats.baseAttack, maxPossibleStats.baseAttack); maxPossibleStats.baseDefense = Math.max(mon.stats.baseDefense, maxPossibleStats.baseDefense); pokemonBaseStamina.push({ id: mon.id, value: mon.stats.baseStamina, }); pokemonBaseAttack.push({ id: mon.id, value: mon.stats.baseAttack, }); pokemonBaseDefense.push({ id: mon.id, value: mon.stats.baseDefense, }); pokemonOrderById[mon.id] = { staminaRank: -1, attackRank: -1, defenseRank: -1, }; }); pokemonBaseStamina.sort((a, b) => { return a.value - b.value; }); pokemonBaseStamina.forEach((stats, index, array) => { pokemonOrderById[stats.id].staminaRank = Math.floor((index / (array.length - 1)) * 100); }); pokemonBaseAttack.sort((a, b) => { return a.value - b.value; }); pokemonBaseAttack.forEach((stats, index, array) => { pokemonOrderById[stats.id].attackRank = Math.floor((index / (array.length - 1)) * 100); }); pokemonBaseDefense.sort((a, b) => { return a.value - b.value; }); pokemonBaseDefense.forEach((stats, index, array) => { pokemonOrderById[stats.id].defenseRank = Math.floor((index / (array.length - 1)) * 100); }); Pokemon.forEach((mon) => { const { name, form } = parseNameAndForm(mon.id, mon.name); const baseAtk = mon.stats.baseAttack; const baseDef = mon.stats.baseDefense; const baseHp = mon.stats.baseStamina; const pokemonDescription = typeof PokemonDescription[mon.id as PokemonId] !== 'undefined' ? PokemonDescription[mon.id as PokemonId] : {}; const pokemon : ILeaguePokemon = { id: mon.id as PokemonId, name: pokemonDescription.name || name || 'MissingNo.', category: pokemonDescription.category || '', form, dex: mon.dex, types: { type1: mon.types[0].name.toLowerCase() as Type, type2: mon.types[1] ? mon.types[1].name.toLowerCase() as Type : null, }, stats: mon.stats, statsRank: pokemonOrderById[mon.id], family: mon.family.id, pvp: { great: [], ultra: [], }, }; // keep track of family order and membership if (typeof familyEncountered[pokemon.family] === 'undefined') { familyOrder.push(pokemon.family); familyEncountered[pokemon.family] = []; } familyEncountered[pokemon.family].push(pokemon); if (typeof mon.evolution.pastBranch === 'undefined' && typeof familyEvolutionOrder[pokemon.family] === 'undefined' && pokemon.id.indexOf('_ALOLA') === -1 // because RAICHU_ALOLA shows up before PICHU, but PICHU is the family origin ) { familyEvolutionOrder[pokemon.family] = []; if (mon.forms.length > 0) { mon.forms.forEach((monForm) => { familyEvolutionOrder[pokemon.family].push(monForm.id); }); } else { familyEvolutionOrder[pokemon.family].push(pokemon.id); } // TODO: if `mon.forms.length > 0`, there's a chance the order will get weird by doing this: if (typeof mon.evolution.futureBranches !== 'undefined') { (function traverseEvolutionBranches(root) { root.forEach((evolution) => { familyEvolutionOrder[pokemon.family].push(evolution.id); // unfortunate workaround for typescript limitation in JSON parsing being TOO good... // if (typeof evolution !== 'undefined') { if ('futureBranches' in evolution) { // traverseEvolutionBranches(evolution.futureBranches); traverseEvolutionBranches(evolution['futureBranches']); } }); })(mon.evolution.futureBranches); } } // calculate stats for all possible IVs const combinedStatsDistribution : IStatsDistribution = { great: {}, ultra: {}, }; for (let ivHp = 15; ivHp >= 0; ivHp--) { for (let ivAtk = 15; ivAtk >= 0; ivAtk--) { for (let ivDef = 15; ivDef >= 0; ivDef--) { let pokemonWithIvs : IStats; const cpMultiplier = (baseAtk + ivAtk) * Math.sqrt(baseDef + ivDef) * Math.sqrt(baseHp + ivHp); Object.keys(maxCpByLeague).forEach((key) => { const league = key as League; const maxCp = maxCpByLeague[league]; const maxLeagueLevelMultiplierIndex = getClosestCpMultiplierIndex(Math.sqrt((maxCp * 10) / cpMultiplier)); const maxLeagueLevelMultiplier = LevelMultipliers[maxLeagueLevelMultiplierIndex]; const maxLeagueCp = Math.floor((cpMultiplier * Math.pow(maxLeagueLevelMultiplier, 2)) / 10); const maxLeagueLevel = (maxLeagueLevelMultiplierIndex + 2) / 2; pokemonWithIvs = { cp: maxLeagueCp, level: maxLeagueLevel, ivHp, ivAtk, ivDef, hp: Math.floor((baseHp + ivHp) * maxLeagueLevelMultiplier), atk: Math.floor((baseAtk + ivAtk) * maxLeagueLevelMultiplier), def: Math.floor((baseDef + ivDef) * maxLeagueLevelMultiplier), total: 0, speciesGrade: Grade.F, metaGrade: Grade.F, }; pokemonWithIvs.total = pokemonWithIvs.hp + pokemonWithIvs.atk + pokemonWithIvs.def; const combinedStats = maxLeagueCp + pokemonWithIvs.total; combinedStatsDistribution[league][combinedStats] = combinedStatsDistribution[league][combinedStats] || []; combinedStatsDistribution[league][combinedStats].push(pokemonWithIvs); }); } } } // process the pokemon stats for league-worthiness Object.keys(pokemon.pvp).forEach((key) => { const league = key as League; const orderedCombinedStats = Object.keys(combinedStatsDistribution[league]).map((cpTotal) => parseInt(cpTotal, 10)); orderedCombinedStats.sort((a, b) => a - b); const len = orderedCombinedStats.length - 1; const offset = orderedCombinedStats[1]; const max = orderedCombinedStats[len] - offset; // index 0 is always `Grade.S` for (let index = len; index >= 0; index--) { const combinedStats = orderedCombinedStats[index]; const percent = (combinedStats - offset) / max; // remove all `Grade.F` stats (to save space in the DB) if (percent < 0.6) { delete combinedStatsDistribution[league][combinedStats]; continue; } combinedStatsDistribution[league][combinedStats].forEach((pokemonStats) => { if (index === len) { pokemonStats.speciesGrade = Grade.S; } else { if (percent >= 0.9) { pokemonStats.speciesGrade = Grade.A; } else if (percent >= 0.8) { pokemonStats.speciesGrade = Grade.B; } else if (percent >= 0.7) { pokemonStats.speciesGrade = Grade.C; } else if (percent >= 0.6) { pokemonStats.speciesGrade = Grade.D; } } pokemon.pvp[league].push(pokemonStats); }); combinedStatsDistribution[league][combinedStats].sort((a, b) => { if (a.total !== b.total) { return a.total < b.total ? -1 : 1; } if (a.level !== b.level) { return a.level < b.level ? -1 : 1; } return 0; }); } }); fs.mkdir(outPath, { recursive: true }, () => { fs.writeFile(outPath + mon.id + '.json', JSON.stringify(pokemon), (err) => { if (err) { /* tslint:disable-next-line:no-console */ return console.error(mon.name, err); } }); }); }); const pokemonOrder : Array = []; familyOrder.forEach((familyId) => { familyEvolutionOrder[familyId].forEach((id, order) => { familyEncountered[familyId].some((pokemon, index) => { if (id === pokemon.id && order !== index) { familyEncountered[familyId].splice(order, 0, familyEncountered[familyId].splice(index, 1)[0]); return true; } return false; }); }); familyEncountered[familyId].forEach((leaguePokemon) => { const { pvp, ...pokemon } = leaguePokemon; pokemonOrder.push(pokemon); }); }); fs.mkdir(outPath, { recursive: true }, () => { fs.writeFile(outPath + 'order.json', JSON.stringify(pokemonOrder), (err) => { if (err) { /* tslint:disable-next-line:no-console */ return console.error('order', err); } }); }); // TODO: add moves fs.mkdir(outPath, { recursive: true }, () => { fs.writeFile(outPath + 'config.json', JSON.stringify({ maxPossibleStats }), (err) => { if (err) { /* tslint:disable-next-line:no-console */ return console.error('order', err); } }); });