diff --git a/src/classes/PokeApi.ts b/src/classes/PokeApi.ts index 50dc954..b65a2c6 100644 --- a/src/classes/PokeApi.ts +++ b/src/classes/PokeApi.ts @@ -12,6 +12,7 @@ import type { PokeApiTypes, } from '../typings/pokeapi' import type { MoveProto, PokemonIdProto, TypeProto } from '../typings/protos' +import { sortTempEvolutions } from '../utils/tempEvolutions' import Masterfile from './Masterfile' export default class PokeApi extends Masterfile { @@ -112,6 +113,51 @@ export default class PokeApi extends Masterfile { return url } + private buildStatMap(stats: PokeApiStats['stats']): { [stat: string]: number } { + const baseStats: { [stat: string]: number } = {} + stats.forEach((stat) => { + baseStats[stat.stat.name] = stat.base_stat + }) + return baseStats + } + + private typeNameToTypeId(typeName: string): number { + return Rpc.HoloPokemonType[`POKEMON_TYPE_${typeName.toUpperCase()}` as TypeProto] + } + + private mapTypeIds(types: PokeApiStats['types']): number[] { + return types + .map((type) => this.typeNameToTypeId(type.type.name)) + .sort((a, b) => a - b) + } + + private mapNamedTypeIds(types: { name: string }[]): number[] { + return types + .map((type) => this.typeNameToTypeId(type.name)) + .sort((a, b) => a - b) + } + + private calculatePogoStats( + baseStats: { [stat: string]: number }, + nerf: boolean = false, + ): { attack: number; defense: number; stamina: number } { + 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: number, special: number, @@ -158,12 +204,14 @@ export default class PokeApi extends Masterfile { ) } - megaLookup(id: string, type: string): string | 1 | 2 | 3 { + megaLookup(id: string, type: string): string | 1 | 2 | 3 | 5 { 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 } @@ -210,59 +258,16 @@ export default class PokeApi extends Masterfile { this.buildUrl(`pokemon/${id}`), ) - const baseStats: { [stat: string]: number } = {} - statsData.stats.forEach((stat) => { - baseStats[stat.stat.name] = stat.base_stat - }) - const initial: { - attack: number - defense: number - stamina: number - cp?: number - } = { - attack: PokeApi.attack( - baseStats.attack, - baseStats['special-attack'], - baseStats.speed, - ), - defense: PokeApi.defense( - baseStats.defense, - baseStats['special-defense'], - baseStats.speed, - ), - stamina: PokeApi.stamina(baseStats.hp), - } - initial.cp = this.cp( + 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 = { - attack: - initial.cp > 4000 - ? PokeApi.attack( - baseStats.attack, - baseStats['special-attack'], - baseStats.speed, - true, - ) - : initial.attack, - defense: - initial.cp > 4000 - ? PokeApi.defense( - baseStats.defense, - baseStats['special-defense'], - baseStats.speed, - true, - ) - : initial.defense, - stamina: - initial.cp > 4000 - ? PokeApi.stamina(baseStats.hp, true) - : initial.stamina, - } + const nerfCheck = cp > 4000 ? this.calculatePogoStats(baseStats, true) : initial this.baseStats[id] = { pokemonName: this.capitalize(statsData.name), quickMoves: statsData.moves @@ -294,14 +299,7 @@ export default class PokeApi extends Masterfile { stamina: this.inconsistentStats[id] ? this.inconsistentStats[id].stamina || nerfCheck.stamina : nerfCheck.stamina, - types: statsData.types - .map( - (type) => - Rpc.HoloPokemonType[ - `POKEMON_TYPE_${type.type.name.toUpperCase()}` as TypeProto - ], - ) - .sort((a, b) => a - b), + types: this.mapTypeIds(statsData.types), unreleased: true, } } catch (e) { @@ -326,7 +324,7 @@ export default class PokeApi extends Masterfile { Rpc.HoloPokemonId[ evoData.evolves_from_species.name .toUpperCase() - .replace('-', '_') as PokemonIdProto + .replace(/-/g, '_') as PokemonIdProto ] ?? +evoData.evolves_from_species.url.split('/').at(-2) if (prevEvoId) { if (!this.baseStats[prevEvoId]) { @@ -351,7 +349,7 @@ export default class PokeApi extends Masterfile { 'Unable to find proto ID for', evoData.evolves_from_species.name .toUpperCase() - .replace('-', '_'), + .replace(/-/g, '_'), ) } } @@ -364,152 +362,100 @@ export default class PokeApi extends Masterfile { } async tempEvoApi(parsedPokemon: AllPokemon) { - const theoretical = { - mega: [ - 'alakazam-mega', - 'kangaskhan-mega', - 'pinsir-mega', - 'aerodactyl-mega', - 'mewtwo-mega-x', - 'mewtwo-mega-y', - 'steelix-mega', - 'scizor-mega', - 'heracross-mega', - 'tyranitar-mega', - 'sceptile-mega', - 'blaziken-mega', - 'swampert-mega', - 'gardevoir-mega', - 'sableye-mega', - 'mawile-mega', - 'aggron-mega', - 'medicham-mega', - 'sharpedo-mega', - 'camerupt-mega', - 'banette-mega', - 'absol-mega', - 'glalie-mega', - 'garchomp-mega', - 'lucario-mega', - 'latias-mega', - 'latios-mega', - 'rayquaza-mega', - 'metagross-mega', - 'salamence-mega', - 'gallade-mega', - 'audino-mega', - 'diancie-mega', - 'clefable-mega', - 'victreebel-mega', - 'starmie-mega', - 'dragonite-mega', - 'meganium-mega', - 'feraligatr-mega', - 'skarmory-mega', - 'froslass-mega', - 'emboar-mega', - 'excadrill-mega', - 'scolipede-mega', - 'scrafty-mega', - 'eelektross-mega', - 'chandelure-mega', - 'chesnaught-mega', - 'delphox-mega', - 'greninja-mega', - 'pyroar-mega', - 'floette-mega', - 'malamar-mega', - 'barbaracle-mega', - 'dragalge-mega', - 'hawlucha-mega', - 'zygarde-mega', - 'drampa-mega', - 'falinks-mega', - ], - } + const discoveredMega = + ( + (await this.fetch( + this.buildUrl('pokemon?limit=100000&offset=0'), + )) as { results?: BasePokeApiStruct[] } + )?.results + ?.map((pokemon) => pokemon.name) + ?.filter((name) => /-mega(?:-[xyz])?$/.test(name)) || [] - for (const [type, ids] of Object.entries(theoretical)) { - this.tempEvos[type] = {} - await Promise.all( - ids.map(async (id) => { - try { - const pokemonId = - Rpc.HoloPokemonId[ - id.split('-')[0].toUpperCase() as PokemonIdProto - ] - const statsData: PokeApiStats = await this.fetch( - this.buildUrl(`pokemon/${id}`), - ) - const baseStats: { [stat: string]: number } = {} - statsData.stats.forEach((stat) => { - baseStats[stat.stat.name] = stat.base_stat - }) - const types = statsData.types - .map( - (type) => - Rpc.HoloPokemonType[ - `POKEMON_TYPE_${type.type.name.toUpperCase()}` as TypeProto - ], - ) - .sort((a, b) => a - b) + const type = 'mega' + this.tempEvos[type] = {} + const megaIds = Array.from(new Set(discoveredMega)) - const newTheoretical: TempEvolutions = { - tempEvoId: this.megaLookup(id, type), - attack: PokeApi.attack( - baseStats.attack, - baseStats['special-attack'], - baseStats.speed, - ), - defense: PokeApi.defense( - baseStats.defense, - baseStats['special-defense'], - baseStats.speed, - ), - stamina: PokeApi.stamina(baseStats.hp), - types: this.compare(types, parsedPokemon[pokemonId].types) - ? undefined - : types, - unreleased: true, - } - if (!this.tempEvos[type][pokemonId]) { - this.tempEvos[type][pokemonId] = {} - } - if (!this.tempEvos[type][pokemonId].tempEvolutions) { - this.tempEvos[type][pokemonId].tempEvolutions = [] - } - if ( - !parsedPokemon[pokemonId].tempEvolutions || - (parsedPokemon[pokemonId].tempEvolutions && - !parsedPokemon[pokemonId].tempEvolutions.some( - (temp) => temp.tempEvoId === newTheoretical.tempEvoId, - )) - ) { - this.tempEvos[type][pokemonId].tempEvolutions.push(newTheoretical) + await Promise.all( + megaIds.map(async (id) => { + try { + const statsData: PokeApiStats = await this.fetch( + this.buildUrl(`pokemon/${id}`), + ) + if (!statsData) return + const pokemonId = + (statsData.species?.name + ? Rpc.HoloPokemonId[ + statsData.species.name + .toUpperCase() + .replace(/-/g, '_') as PokemonIdProto + ] + : 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: TempEvolutions = { + 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 } - this.tempEvos[type][pokemonId].tempEvolutions.sort((a, b) => - typeof a.tempEvoId === 'number' && typeof b.tempEvoId === 'number' - ? a.tempEvoId - b.tempEvoId - : a.tempEvoId.toString().localeCompare(b.tempEvoId.toString()), - ) - } catch (e) { - console.warn(e, `Failed to parse PokeApi ${type} Evos for ${id}`) + return } - }), - ) - } + + this.tempEvos[type][pokemonId].tempEvolutions = sortTempEvolutions([ + ...this.tempEvos[type][pokemonId].tempEvolutions, + newTheoretical, + ]) + } catch (e) { + console.warn(e, `Failed to parse PokeApi ${type} Evos for ${id}`) + } + }), + ) } async typesApi() { - const getTypeIds = (types: { name: string }[]) => - types - .map( - (type) => - Rpc.HoloPokemonType[ - `POKEMON_TYPE_${type.name.toUpperCase()}` as TypeProto - ], - ) - .sort((a, b) => a - b) - await Promise.all( Object.entries(Rpc.HoloPokemonType).map(async ([type, id]) => { try { @@ -528,12 +474,12 @@ export default class PokeApi extends Masterfile { ) : { damage_relations: {} } this.types[id] = { - strengths: id ? getTypeIds(double_damage_to) : [], - weaknesses: id ? getTypeIds(double_damage_from) : [], - veryWeakAgainst: id ? getTypeIds(no_damage_to) : [], - immunes: id ? getTypeIds(no_damage_from) : [], - weakAgainst: id ? getTypeIds(half_damage_to) : [], - resistances: id ? getTypeIds(half_damage_from) : [], + 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) diff --git a/src/classes/Pokemon.ts b/src/classes/Pokemon.ts index 5f9d76d..d2d88cf 100644 --- a/src/classes/Pokemon.ts +++ b/src/classes/Pokemon.ts @@ -26,6 +26,7 @@ import type { QuestTypeProto, TypeProto, } from '../typings/protos' +import { mergeTempEvolutions, sortTempEvolutions } from '../utils/tempEvolutions' import Masterfile from './Masterfile' import PokeApi from './PokeApi' import PokemonOverrides from './PokemonOverrides' @@ -297,9 +298,13 @@ export default class Pokemon extends Masterfile { const tempEvolutions: TempEvolutions[] = mfObject .filter((tempEvo) => tempEvo.stats) .map((tempEvo) => { + const resolvedTempEvoId = + Rpc.HoloTemporaryEvolutionId[tempEvo.tempEvoId as MegaProto] ?? + (tempEvo.tempEvoId === 'TEMP_EVOLUTION_MEGA_Z' + ? 5 + : tempEvo.tempEvoId) const newTempEvolution: TempEvolutions = { - tempEvoId: - Rpc.HoloTemporaryEvolutionId[tempEvo.tempEvoId as MegaProto], + tempEvoId: resolvedTempEvoId, } switch (true) { case tempEvo.stats.baseAttack !== primaryForm.attack: @@ -333,9 +338,7 @@ export default class Pokemon extends Masterfile { } return newTempEvolution }) - return tempEvolutions.sort( - (a, b) => (a.tempEvoId as number) - (b.tempEvoId as number), - ) + return sortTempEvolutions(tempEvolutions) } catch (e) { console.warn( e, @@ -1178,12 +1181,10 @@ export default class Pokemon extends Masterfile { ) { Object.keys(tempEvos[category]).forEach((id) => { try { - const tempEvolutions = [ - ...tempEvos[category][id].tempEvolutions, - ...(this.parsedPokemon[id].tempEvolutions - ? this.parsedPokemon[id].tempEvolutions - : []), - ] + const tempEvolutions = mergeTempEvolutions( + tempEvos[category][id].tempEvolutions, + this.parsedPokemon[id].tempEvolutions, + ) this.parsedPokemon[id] = { ...this.parsedPokemon[id], tempEvolutions, diff --git a/src/classes/Translations.ts b/src/classes/Translations.ts index c37653e..b7f38f3 100644 --- a/src/classes/Translations.ts +++ b/src/classes/Translations.ts @@ -932,6 +932,19 @@ export default class Translations extends Masterfile { `${this.options.prefix.evolutions}${id}` ] = this.capitalize(name.replace('TEMP_EVOLUTION_', '')) }) + if ( + !Object.prototype.hasOwnProperty.call( + Rpc.HoloTemporaryEvolutionId, + 'TEMP_EVOLUTION_MEGA_Z', + ) && + !this.parsedTranslations[locale].misc[ + `${this.options.prefix.evolutions}5` + ] + ) { + this.parsedTranslations[locale].misc[ + `${this.options.prefix.evolutions}5` + ] = this.capitalize('MEGA_Z') + } Object.entries(Rpc.PokemonDisplayProto.Alignment).forEach((proto) => { const [name, id] = proto this.parsedTranslations[locale].misc[ diff --git a/src/utils/tempEvolutions.ts b/src/utils/tempEvolutions.ts new file mode 100644 index 0000000..faedfdd --- /dev/null +++ b/src/utils/tempEvolutions.ts @@ -0,0 +1,73 @@ +import type { TempEvolutions } from '../typings/dataTypes' + +export type TempEvoId = TempEvolutions['tempEvoId'] + +/** + * Returns a stable, type-sensitive key for a temp evolution ID. + * This keeps `1` and `'1'` distinct. + */ +export const tempEvoIdKey = (tempEvoId: TempEvoId): string => + `${typeof tempEvoId}:${tempEvoId}` + +/** + * Sort comparator for temp evolution IDs: + * - Numbers sort before strings + * - Numbers sort numerically (ascending) + * - Strings sort lexicographically (localeCompare) + */ +export const compareTempEvoId = (a: TempEvoId, b: TempEvoId): number => { + const aIsNumber = typeof a === 'number' + const bIsNumber = typeof b === 'number' + + if (aIsNumber && bIsNumber) return a - b + if (aIsNumber) return -1 + if (bIsNumber) return 1 + return a.toString().localeCompare(b.toString()) +} + +/** + * Returns a new array sorted by `tempEvoId` without mutating the input array. + */ +export const sortTempEvolutions = ( + tempEvolutions: TempEvolutions[], +): TempEvolutions[] => + [...tempEvolutions].sort((a, b) => compareTempEvoId(a.tempEvoId, b.tempEvoId)) + +/** + * Deduplicates temp evolutions by `tempEvoId` (type-sensitive) and returns them sorted. + * Defaults to preferring the last entry when duplicates exist. + */ +export const dedupeTempEvolutions = ( + tempEvolutions: (TempEvolutions | undefined | null)[], + options: { prefer?: 'first' | 'last' } = {}, +): TempEvolutions[] => { + const prefer = options.prefer ?? 'last' + const deduped = new Map() + + for (const tempEvo of tempEvolutions) { + if (!tempEvo) continue + const key = tempEvoIdKey(tempEvo.tempEvoId) + + if (prefer === 'first') { + if (!deduped.has(key)) deduped.set(key, tempEvo) + continue + } + + deduped.set(key, tempEvo) + } + + return sortTempEvolutions(Array.from(deduped.values())) +} + +/** + * Merges estimated + actual temp evolutions, preferring actual values for duplicates. + */ +export const mergeTempEvolutions = ( + estimated: (TempEvolutions | undefined | null)[] | undefined, + actual: (TempEvolutions | undefined | null)[] | undefined, +): TempEvolutions[] => { + const estimatedList = Array.isArray(estimated) ? estimated : [] + const actualList = Array.isArray(actual) ? actual : [] + + return dedupeTempEvolutions([...estimatedList, ...actualList], { prefer: 'last' }) +} diff --git a/static/baseStats.json b/static/baseStats.json index 032e67a..4f32a32 100644 --- a/static/baseStats.json +++ b/static/baseStats.json @@ -13,7 +13,9 @@ "evoId": 899, "formId": 3218 } - ] + ], + "legendary": false, + "mythic": false }, "290": { "evolutions": [ @@ -29,7 +31,7 @@ "evolutions": [ { "evoId": 1018, - "formId": 0 + "formId": 3330 } ], "legendary": false, diff --git a/static/tempEvos.json b/static/tempEvos.json index f6e73cc..3f251b6 100644 --- a/static/tempEvos.json +++ b/static/tempEvos.json @@ -1,5 +1,23 @@ { "mega": { + "26": { + "tempEvolutions": [ + { + "tempEvoId": 2, + "attack": 277, + "defense": 203, + "stamina": 155, + "unreleased": true + }, + { + "tempEvoId": 3, + "attack": 339, + "defense": 157, + "stamina": 155, + "unreleased": true + } + ] + }, "36": { "tempEvolutions": [ { @@ -15,9 +33,6 @@ } ] }, - "65": { - "tempEvolutions": [] - }, "71": { "tempEvolutions": [ { @@ -29,9 +44,6 @@ } ] }, - "115": { - "tempEvolutions": [] - }, "121": { "tempEvolutions": [ { @@ -43,12 +55,6 @@ } ] }, - "127": { - "tempEvolutions": [] - }, - "142": { - "tempEvolutions": [] - }, "149": { "tempEvolutions": [ { @@ -112,15 +118,6 @@ } ] }, - "208": { - "tempEvolutions": [] - }, - "212": { - "tempEvolutions": [] - }, - "214": { - "tempEvolutions": [] - }, "227": { "tempEvolutions": [ { @@ -132,71 +129,75 @@ } ] }, - "248": { - "tempEvolutions": [] - }, - "254": { - "tempEvolutions": [] - }, - "257": { - "tempEvolutions": [] - }, - "260": { - "tempEvolutions": [] - }, - "282": { - "tempEvolutions": [] - }, - "302": { - "tempEvolutions": [] - }, - "303": { - "tempEvolutions": [] - }, - "306": { - "tempEvolutions": [] - }, - "308": { - "tempEvolutions": [] - }, - "319": { - "tempEvolutions": [] - }, - "323": { - "tempEvolutions": [] - }, - "354": { - "tempEvolutions": [] + "358": { + "tempEvolutions": [ + { + "tempEvoId": 1, + "attack": 244, + "defense": 228, + "stamina": 181, + "types": [ + 9, + 14 + ], + "unreleased": true + } + ] }, "359": { - "tempEvolutions": [] - }, - "362": { - "tempEvolutions": [] - }, - "373": { - "tempEvolutions": [] - }, - "376": { - "tempEvolutions": [] - }, - "380": { - "tempEvolutions": [] - }, - "381": { - "tempEvolutions": [] + "tempEvolutions": [ + { + "tempEvoId": 5, + "attack": 332, + "defense": 138, + "stamina": 163, + "types": [ + 8, + 17 + ], + "unreleased": true + } + ] }, - "384": { - "tempEvolutions": [] + "398": { + "tempEvolutions": [ + { + "tempEvoId": 1, + "attack": 278, + "defense": 207, + "stamina": 198, + "types": [ + 2, + 3 + ], + "unreleased": true + } + ] }, "445": { - "tempEvolutions": [] + "tempEvolutions": [ + { + "tempEvoId": 5, + "attack": 321, + "defense": 196, + "stamina": 239, + "types": [ + 16 + ], + "unreleased": true + } + ] }, "448": { - "tempEvolutions": [] - }, - "475": { - "tempEvolutions": [] + "tempEvolutions": [ + { + "tempEvoId": 5, + "attack": 359, + "defense": 161, + "stamina": 172, + "unreleased": true + } + ] }, "478": { "tempEvolutions": [ @@ -209,6 +210,28 @@ } ] }, + "485": { + "tempEvolutions": [ + { + "tempEvoId": 1, + "attack": 331, + "defense": 252, + "stamina": 209, + "unreleased": true + } + ] + }, + "491": { + "tempEvolutions": [ + { + "tempEvoId": 1, + "attack": 325, + "defense": 265, + "stamina": 172, + "unreleased": true + } + ] + }, "500": { "tempEvolutions": [ { @@ -231,9 +254,6 @@ } ] }, - "531": { - "tempEvolutions": [] - }, "545": { "tempEvolutions": [ { @@ -278,6 +298,17 @@ } ] }, + "623": { + "tempEvolutions": [ + { + "tempEvoId": 1, + "attack": 284, + "defense": 202, + "stamina": 205, + "unreleased": true + } + ] + }, "652": { "tempEvolutions": [ { @@ -333,6 +364,17 @@ } ] }, + "678": { + "tempEvolutions": [ + { + "tempEvoId": 1, + "attack": 288, + "defense": 201, + "stamina": 179, + "unreleased": true + } + ] + }, "687": { "tempEvolutions": [ { @@ -392,8 +434,31 @@ } ] }, - "719": { - "tempEvolutions": [] + "740": { + "tempEvolutions": [ + { + "tempEvoId": 1, + "attack": 266, + "defense": 213, + "stamina": 219, + "unreleased": true + } + ] + }, + "768": { + "tempEvolutions": [ + { + "tempEvoId": 1, + "attack": 260, + "defense": 287, + "stamina": 181, + "types": [ + 7, + 9 + ], + "unreleased": true + } + ] }, "780": { "tempEvolutions": [ @@ -406,6 +471,28 @@ } ] }, + "801": { + "tempEvolutions": [ + { + "tempEvoId": 1, + "attack": 342, + "defense": 239, + "stamina": 190, + "unreleased": true + } + ] + }, + "807": { + "tempEvolutions": [ + { + "tempEvoId": 1, + "attack": 361, + "defense": 180, + "stamina": 204, + "unreleased": true + } + ] + }, "870": { "tempEvolutions": [ { @@ -416,6 +503,50 @@ "unreleased": true } ] + }, + "952": { + "tempEvolutions": [ + { + "tempEvoId": 1, + "attack": 276, + "defense": 170, + "stamina": 163, + "unreleased": true + } + ] + }, + "970": { + "tempEvolutions": [ + { + "tempEvoId": 1, + "attack": 300, + "defense": 214, + "stamina": 195, + "unreleased": true + } + ] + }, + "978": { + "tempEvolutions": [ + { + "tempEvoId": 1, + "attack": 262, + "defense": 232, + "stamina": 169, + "unreleased": true + } + ] + }, + "998": { + "tempEvolutions": [ + { + "tempEvoId": 1, + "attack": 341, + "defense": 227, + "stamina": 251, + "unreleased": true + } + ] } } } \ No newline at end of file diff --git a/tests/tempEvolutions.utils.test.js b/tests/tempEvolutions.utils.test.js new file mode 100644 index 0000000..4e5be12 --- /dev/null +++ b/tests/tempEvolutions.utils.test.js @@ -0,0 +1,64 @@ +const { + compareTempEvoId, + dedupeTempEvolutions, + mergeTempEvolutions, + sortTempEvolutions, + tempEvoIdKey, +} = require('../dist/utils/tempEvolutions') + +describe('tempEvolutions utils', () => { + test('tempEvoIdKey is type-sensitive', () => { + expect(tempEvoIdKey(1)).toBe('number:1') + expect(tempEvoIdKey('1')).toBe('string:1') + expect(tempEvoIdKey(1)).not.toBe(tempEvoIdKey('1')) + }) + + test('compareTempEvoId orders numbers before strings', () => { + expect(compareTempEvoId(1, 2)).toBeLessThan(0) + expect(compareTempEvoId(2, 1)).toBeGreaterThan(0) + expect(compareTempEvoId(1, 'a')).toBeLessThan(0) + expect(compareTempEvoId('a', 1)).toBeGreaterThan(0) + expect(compareTempEvoId('a', 'b')).toBeLessThan(0) + }) + + test('sortTempEvolutions returns a sorted copy without mutating input', () => { + const input = [{ tempEvoId: 'b' }, { tempEvoId: 2 }, { tempEvoId: 1 }, { tempEvoId: 'a' }] + const originalOrder = input.map((e) => e.tempEvoId) + + const sorted = sortTempEvolutions(input) + expect(sorted.map((e) => e.tempEvoId)).toEqual([1, 2, 'a', 'b']) + expect(input.map((e) => e.tempEvoId)).toEqual(originalOrder) + }) + + test('dedupeTempEvolutions skips nullish and prefers last by default', () => { + const list = [ + null, + undefined, + { tempEvoId: 1, attack: 10 }, + { tempEvoId: 1, attack: 20 }, + { tempEvoId: '1', attack: 30 }, + ] + const deduped = dedupeTempEvolutions(list) + expect(deduped.map((e) => [e.tempEvoId, e.attack])).toEqual([ + [1, 20], + ['1', 30], + ]) + }) + + test('dedupeTempEvolutions can prefer first', () => { + const list = [{ tempEvoId: 1, attack: 10 }, { tempEvoId: 1, attack: 20 }] + const deduped = dedupeTempEvolutions(list, { prefer: 'first' }) + expect(deduped.map((e) => [e.tempEvoId, e.attack])).toEqual([[1, 10]]) + }) + + test('mergeTempEvolutions prefers actual for duplicates', () => { + const estimated = [{ tempEvoId: 1, attack: 10 }, { tempEvoId: 'a', attack: 1 }] + const actual = [{ tempEvoId: 1, attack: 99 }] + const merged = mergeTempEvolutions(estimated, actual) + expect(merged.map((e) => [e.tempEvoId, e.attack])).toEqual([ + [1, 99], + ['a', 1], + ]) + }) +}) + diff --git a/tests/tempEvos.test.js b/tests/tempEvos.test.js new file mode 100644 index 0000000..4a3903a --- /dev/null +++ b/tests/tempEvos.test.js @@ -0,0 +1,18 @@ +const { tempEvoIdKey } = require('../dist/utils/tempEvolutions') +const tempEvos = require('../static/tempEvos.json') + +describe('static/tempEvos.json', () => { + test('does not contain duplicate temp evolutions', () => { + Object.entries(tempEvos).forEach(([category, byPokemon]) => { + Object.entries(byPokemon).forEach(([pokemonId, data]) => { + const seen = new Set() + const evolutions = (data && data.tempEvolutions) || [] + evolutions.forEach((evo) => { + const key = tempEvoIdKey(evo.tempEvoId) + expect(seen.has(key)).toBe(false) + seen.add(key) + }) + }) + }) + }) +})