const request = require('node-superfetch'); const { createCanvas } = require('@napi-rs/canvas'); const path = require('path'); const { removeDuplicates, firstUpperCase, delay } = require('../../util/Util'); const { cropToContent } = require('../../util/Canvas'); const missingno = require('../../assets/json/missingno'); const versions = require('../../assets/json/pokedex-location'); module.exports = class Pokemon { constructor(store, data) { this.store = store; this.id = data.id; const slugName = firstUpperCase(data.name).replaceAll('-', ' '); this.name = data.names.length ? data.names.find(entry => entry.language.name === 'en').name : slugName; this.entries = data.flavor_text_entries .filter(entry => entry.language.name === 'en') .map(entry => { return { text: entry.flavor_text.replace(/\n|\f|\r/g, ' '), version: versions[entry.version.name] }; }); this.names = data.names.length ? data.names.map(entry => ({ name: entry.name, language: entry.language.name })) : [{ name: slugName, language: 'en' }]; this.genus = data.genera.length ? `The ${data.genera.filter(entry => entry.language.name === 'en')[0].genus}` : 'Undiscovered Pokémon'; this.genderRate = { male: data.gender_rate === -1 ? 0 : 100 - ((data.gender_rate / 8) * 100), female: data.gender_rate === -1 ? 0 : (data.gender_rate / 8) * 100, genderless: data.gender_rate === -1 }; this.legendary = data.is_legendary; this.mythical = data.is_mythical; this.baby = data.is_baby; this.heldItems = data.missingno ? data.held_items : []; this.varieties = data.varieties.map(variety => { const name = firstUpperCase(variety.pokemon.name .replace(new RegExp(`${this.slug}-?`, 'i'), '') .replaceAll('-', ' ')); return { id: variety.pokemon.name, name: name || null, mega: data.missingno ? false : null, stats: data.missingno ? variety.stats : {}, statsDiffer: data.missingno ? true : null, default: variety.is_default, types: data.missingno ? variety.types : [], abilities: data.missingno ? variety.abilities : [], gameDataCached: data.missingno || false }; }); this.chain = { url: data.evolution_chain ? data.evolution_chain.url : null, data: data.missingno ? missingno.chain : data.evolution_chain ? [] : [data.id] }; this.encountersURL = null; this.encounters = data.missingno ? data.encounters : null; this.height = data.missingno ? data.height : null; this.weight = data.missingno ? data.weight : null; this.moveSet = data.missingno ? data.moveSet : []; this.moveSetVersion = data.missingno ? data.moveSetVersion : null; this.gameDataCached = data.missingno || false; this.gameDataFetching = false; this.missingno = data.missingno || false; this.cry = data.id > store.pokemonCountWithCry ? null : path.join(__dirname, '..', '..', 'assets', 'sounds', 'pokedex', `${data.id}.wav`); this.smogonTiers = data.missingno ? data.smogonTiers : {}; } baseStatTotal(variety) { const found = this.varieties.find(vrity => variety ? vrity.id === variety.toLowerCase() : vrity.default); if (!found) return null; return found.stats.hp + found.stats.atk + found.stats.def + found.stats.sAtk + found.stats.sDef + found.stats.spd; } get pseudo() { if (!this.gameDataCached) return null; if (this.legendary || this.mythical || this.baby || this.missingno) return false; if (this.baseStatTotal() !== 600) return false; if (this.chain.data.length !== 3) return false; return true; } get generation() { if (this.id > 1025) return null; if (this.id >= 906) return 9; if (this.id >= 810) return 8; if (this.id >= 722) return 7; if (this.id >= 650) return 6; if (this.id >= 494) return 5; if (this.id >= 387) return 4; if (this.id >= 252) return 3; if (this.id >= 152) return 2; if (this.id >= 0) return 1; return null; } get class() { if (this.legendary) return 'legendary'; if (this.mythical) return 'mythical'; if (this.baby) return 'baby'; if (this.ultraBeast) return 'ultra beast'; if (this.missingno) return 'glitch'; if (this.pseudo) return 'pseudo'; return 'normal'; } get ultraBeast() { if (!this.gameDataCached) return null; return this.varieties.some(variety => variety.abilities.some(ability => ability.slug === 'beast-boost')); } get mega() { if (!this.gameDataCached) return null; return this.varieties.some(variety => variety.mega); } get displayID() { if (this.missingno) return '???'; return this.id.toString().padStart(3, '0'); } get slug() { return this.store.makeSlug(this.name); } get spriteImageURL() { if (this.missingno) return missingno.sprites.default; return `https://serebii.net/pokemon/art/${this.displayID}.png`; } formSpriteImageURL(variety) { if (this.missingno) { if (variety.toLowerCase() === 'missingno-yellow') return missingno.sprites.yellow; return missingno.sprites.default; } const found = this.varieties.find(vrity => variety ? vrity.id === variety.toLowerCase() : vrity.default); const name = found.default ? '' : found.mega ? found.name.toLowerCase().split(' ').map(n => n.charAt(0)).join('') : found.name.toLowerCase().charAt(0); return `https://serebii.net/pokemon/art/${this.displayID}${name ? `-${name}` : ''}.png`; } get serebiiURL() { if (this.missingno) return missingno.url; return `https://www.serebii.net/pokedex-sv/${this.displayID}.shtml`; } smogonURL(gen) { if (this.missingno) return missingno.url; return `https://www.smogon.com/dex/${gen.toLowerCase()}/pokemon/${this.slug}/`; } async generateBoxImage() { if (!this.store.sprites) await this.store.loadSprites(); const canvas = createCanvas(40, 30); const ctx = canvas.getContext('2d'); const x = 40 * (this.id % 12); const y = Math.floor(this.id / 12) * 30; ctx.drawImage(this.store.sprites, x, y, 40, 30, 0, 0, 40, 30); cropToContent(ctx, canvas, canvas.width, canvas.height); return canvas.toBuffer('image/png'); } async fetchSmogonTiers(...gens) { for (const gen of gens) { if (!this.store.smogonData[gen.toLowerCase()]) await this.store.fetchSmogonData(gen.toLowerCase()); const pkmn = this.store.smogonData[gen.toLowerCase()].find(data => data.id === this.id); this.smogonTiers[gen.toLowerCase()] = pkmn.formats; } return this.smogonTiers; } async fetchGameData() { if (this.gameDataCached) return this; if (this.gameDataFetching) { await delay(1000); return this.fetchGameData(); } this.gameDataFetching = true; await this.fetchDefaultVariety(); await this.fetchHeldItemNames(); await this.fetchOtherVarieties(); await this.fetchChain(); this.gameDataFetching = false; this.gameDataCached = true; return this; } async fetchDefaultVariety() { const defaultVariety = this.varieties.find(variety => variety.default); if (defaultVariety.gameDataCached) return this; const { body: defaultBody } = await request.get(`https://pokeapi.co/api/v2/pokemon/${defaultVariety.id}`); defaultVariety.types.push(...defaultBody.types.map(type => firstUpperCase(type.type.name))); for (const ability of defaultBody.abilities) { const defaultAbilityData = await this.store.abilities.fetch(ability.ability.name); defaultVariety.abilities.push(defaultAbilityData); } defaultVariety.stats = { hp: defaultBody.stats.find(stat => stat.stat.name === 'hp').base_stat, atk: defaultBody.stats.find(stat => stat.stat.name === 'attack').base_stat, def: defaultBody.stats.find(stat => stat.stat.name === 'defense').base_stat, sAtk: defaultBody.stats.find(stat => stat.stat.name === 'special-attack').base_stat, sDef: defaultBody.stats.find(stat => stat.stat.name === 'special-defense').base_stat, spd: defaultBody.stats.find(stat => stat.stat.name === 'speed').base_stat }; defaultVariety.statsDiffer = true; defaultVariety.gameDataCached = true; const inSwordShield = defaultBody.moves .some(move => move.version_group_details.some(mve => mve.version_group.name === 'sword-shield')); const inScarletViolet = defaultBody.moves .some(move => move.version_group_details.some(mve => mve.version_group.name === 'scarlet-violet')); this.moveSetVersion = inScarletViolet ? 'scarlet-violet' : inSwordShield ? 'sword-shield' : 'ultra-sun-ultra-moon'; this.height = defaultBody.height * 3.94; this.weight = defaultBody.weight * 0.2205; this.encountersURL = defaultBody.location_area_encounters; await this.fetchMoves(defaultBody.moves); await this.fetchHeldItems(defaultBody.held_items); return this; } async fetchOtherVarieties() { const defaultVariety = this.varieties.find(variety => variety.default); for (const variety of this.varieties) { if (variety.id === defaultVariety.id) continue; if (variety.gameDataCached) continue; const { body } = await request.get(`https://pokeapi.co/api/v2/pokemon/${variety.id}`); const { body: formBody } = await request.get(`https://pokeapi.co/api/v2/pokemon-form/${variety.id}`); variety.types.push(...body.types.map(type => firstUpperCase(type.type.name))); variety.mega = formBody.is_mega || false; variety.stats = { hp: body.stats.find(stat => stat.stat.name === 'hp').base_stat, atk: body.stats.find(stat => stat.stat.name === 'attack').base_stat, def: body.stats.find(stat => stat.stat.name === 'defense').base_stat, sAtk: body.stats.find(stat => stat.stat.name === 'special-attack').base_stat, sDef: body.stats.find(stat => stat.stat.name === 'special-defense').base_stat, spd: body.stats.find(stat => stat.stat.name === 'speed').base_stat }; const baseStats = defaultVariety.stats; variety.statsDiffer = !(baseStats.hp === variety.stats.hp && baseStats.atk === variety.stats.atk && baseStats.def === variety.stats.def && baseStats.sAtk === variety.stats.sAtk && baseStats.sDef === variety.stats.sDef && baseStats.spd === variety.stats.spd); for (const ability of body.abilities) { const abilityData = await this.store.abilities.fetch(ability.ability.name); variety.abilities.push(abilityData); } variety.gameDataCached = true; } return this.varieties; } async fetchMoves(moves) { for (const move of moves) { const versionGroup = move.version_group_details.find(mve => mve.version_group.name === this.moveSetVersion); if (!versionGroup || !versionGroup.level_learned_at) continue; const moveData = await this.store.moves.fetch(move.move.name); if (this.moveSet.some(mve => mve.move.id === moveData.id)) continue; this.moveSet.push({ move: moveData, level: versionGroup.level_learned_at }); } this.moveSet = this.moveSet.sort((a, b) => a.level - b.level); return this.moveSet; } fetchHeldItems(heldItems) { this.heldItems = heldItems .filter(item => item.version_details.some(version => { const inScarletViolet = version.version.name === 'scarlet' || version.version.name === 'violet'; const inSwordShield = version.version.name === 'sword' || version.version.name === 'shield'; const inUsUm = version.version.name === 'ultra-sun' || version.version.name === 'ultra-moon'; if (inScarletViolet) return true; if (inSwordShield) return true; if (!inSwordShield && !inScarletViolet && inUsUm) return true; return false; })) .map(item => { const inScarletViolet = item.version_details .some(version => version.version.name === 'scarlet' || version.version.name === 'violet'); const inSwordShield = item.version_details .some(version => version.version.name === 'sword' || version.version.name === 'shield'); const { rarity } = item.version_details .find(version => { if (inScarletViolet) return true; if (inSwordShield) return true; const sunMoon = version.version.name === 'ultra-sun' || version.version.name === 'ultra-moon'; if (!inSwordShield && !inScarletViolet && sunMoon) return true; return false; }); return { data: null, slug: item.item.name, rarity }; }); return this.heldItems; } async fetchChain() { if (this.chain.data.length) return this.chain.data; const { body } = await request.get(this.chain.url); const basePkmn = await this.store.fetch(body.chain.species.name); this.chain.data.push(basePkmn.id); if (body.chain.evolves_to.length) { const evolution1 = body.chain.evolves_to; if (!evolution1) return this.chain.data; const evos1 = []; const evos2 = []; for (const evolution of evolution1) { const pkmn = await this.store.fetch(evolution.species.name); evos1.push(pkmn.id); if (evolution.evolves_to) { for (const evolution2 of evolution.evolves_to) { const pkmn2 = await this.store.fetch(evolution2.species.name); evos2.push(pkmn2.id); } } } this.chain.data.push(evos1.length === 1 ? evos1[0] : evos1); if (evos2.length) this.chain.data.push(evos2.length === 1 ? evos2[0] : evos2); } return this.chain.data; } async fetchHeldItemNames() { for (const item of this.heldItems) { const data = await this.store.items.fetch(item.slug); item.data = data; } return this.heldItems; } async fetchEncounters() { if (!this.encountersURL) return null; if (this.encounters) return this.encounters; const { body } = await request.get(this.encountersURL); if (!body.length) { this.encounters = body; return body; } this.encounters = []; for (const encounter of body) { if (!encounter.version_details.some(version => versions[version.version.name])) continue; const { body: encounterBody } = await request.get(encounter.location_area.url); const { body: locationBody } = await request.get(encounterBody.location.url); let name = locationBody.names.find(nm => nm.language?.name === 'en')?.name; if (!name) name = firstUpperCase(locationBody.name.replace(/-/g, ' ')); this.encounters.push({ name, versions: encounter.version_details .filter(version => versions[version.version.name]) .map(version => version.version.name) }); } return this.encounters; } };