From 15d74e3bfacd15a827e0feac00a831687a5471ab Mon Sep 17 00:00:00 2001 From: Mygod Date: Mon, 15 Dec 2025 18:40:05 -0800 Subject: [PATCH 1/9] feat: mega dimension megas --- src/classes/PokeApi.ts | 57 +++++--- static/baseStats.json | 6 +- static/tempEvos.json | 292 ++++++++++++++++++++++++++++------------- 3 files changed, 244 insertions(+), 111 deletions(-) diff --git a/src/classes/PokeApi.ts b/src/classes/PokeApi.ts index cbf9fdf..cce241d 100644 --- a/src/classes/PokeApi.ts +++ b/src/classes/PokeApi.ts @@ -425,18 +425,40 @@ export default class PokeApi extends Masterfile { ], } + const discoveredMega = + ( + (await this.fetch( + this.buildUrl('pokemon?limit=100000&offset=0'), + )) as { results?: BasePokeApiStruct[] } + )?.results + ?.map((pokemon) => pokemon.name) + ?.filter((name) => /-mega(?:-x|-y)?$/.test(name)) || [] + for (const [type, ids] of Object.entries(theoretical)) { this.tempEvos[type] = {} + const combinedIds = Array.from( + new Set([...ids, ...(type === 'mega' ? discoveredMega : [])]), + ) await Promise.all( - ids.map(async (id) => { + combinedIds.map(async (id) => { try { - const pokemonId = - Rpc.HoloPokemonId[ - id.split('-')[0].toUpperCase() as PokemonIdProto - ] 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: { [stat: string]: number } = {} statsData.stats.forEach((stat) => { baseStats[stat.stat.name] = stat.base_stat @@ -450,6 +472,8 @@ export default class PokeApi extends Masterfile { ) .sort((a, b) => a - b) + const baseTypes = + parsedPokemon[pokemonId]?.types || this.baseStats[pokemonId]?.types const newTheoretical: TempEvolutions = { tempEvoId: this.megaLookup(id, type), attack: PokeApi.attack( @@ -463,26 +487,25 @@ export default class PokeApi extends Masterfile { baseStats.speed, ), stamina: PokeApi.stamina(baseStats.hp), - types: this.compare(types, parsedPokemon[pokemonId].types) - ? undefined - : types, + 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 = [] } - if ( - !parsedPokemon[pokemonId].tempEvolutions || - (parsedPokemon[pokemonId].tempEvolutions && - !parsedPokemon[pokemonId].tempEvolutions.some( - (temp) => temp.tempEvoId === newTheoretical.tempEvoId, - )) - ) { - this.tempEvos[type][pokemonId].tempEvolutions.push(newTheoretical) - } + + this.tempEvos[type][pokemonId].tempEvolutions.push(newTheoretical) this.tempEvos[type][pokemonId].tempEvolutions.sort((a, b) => typeof a.tempEvoId === 'number' && typeof b.tempEvoId === 'number' ? a.tempEvoId - b.tempEvoId diff --git a/static/baseStats.json b/static/baseStats.json index 032e67a..4dd36a0 100644 --- a/static/baseStats.json +++ b/static/baseStats.json @@ -29,11 +29,9 @@ "evolutions": [ { "evoId": 1018, - "formId": 0 + "formId": 3330 } - ], - "legendary": false, - "mythic": false + ] }, "902": { "pokemonName": "Basculegion-male", diff --git a/static/tempEvos.json b/static/tempEvos.json index f6e73cc..711169e 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,35 @@ } ] }, - "248": { - "tempEvolutions": [] - }, - "254": { - "tempEvolutions": [] - }, - "257": { - "tempEvolutions": [] - }, - "260": { - "tempEvolutions": [] - }, - "282": { - "tempEvolutions": [] - }, - "302": { - "tempEvolutions": [] - }, - "303": { - "tempEvolutions": [] - }, - "306": { - "tempEvolutions": [] - }, - "308": { - "tempEvolutions": [] - }, - "319": { - "tempEvolutions": [] - }, - "323": { - "tempEvolutions": [] - }, - "354": { - "tempEvolutions": [] - }, - "359": { - "tempEvolutions": [] - }, - "362": { - "tempEvolutions": [] - }, - "373": { - "tempEvolutions": [] - }, - "376": { - "tempEvolutions": [] - }, - "380": { - "tempEvolutions": [] - }, - "381": { - "tempEvolutions": [] - }, - "384": { - "tempEvolutions": [] - }, - "445": { - "tempEvolutions": [] - }, - "448": { - "tempEvolutions": [] + "358": { + "tempEvolutions": [ + { + "tempEvoId": 1, + "attack": 244, + "defense": 228, + "stamina": 181, + "types": [ + 9, + 14 + ], + "unreleased": true + } + ] }, - "475": { - "tempEvolutions": [] + "398": { + "tempEvolutions": [ + { + "tempEvoId": 1, + "attack": 278, + "defense": 207, + "stamina": 198, + "types": [ + 2, + 3 + ], + "unreleased": true + } + ] }, "478": { "tempEvolutions": [ @@ -209,6 +170,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 +214,6 @@ } ] }, - "531": { - "tempEvolutions": [] - }, "545": { "tempEvolutions": [ { @@ -278,6 +258,17 @@ } ] }, + "623": { + "tempEvolutions": [ + { + "tempEvoId": 1, + "attack": 284, + "defense": 202, + "stamina": 205, + "unreleased": true + } + ] + }, "652": { "tempEvolutions": [ { @@ -333,6 +324,17 @@ } ] }, + "678": { + "tempEvolutions": [ + { + "tempEvoId": 1, + "attack": 288, + "defense": 201, + "stamina": 179, + "unreleased": true + } + ] + }, "687": { "tempEvolutions": [ { @@ -392,8 +394,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 +431,35 @@ } ] }, + "801": { + "tempEvolutions": [ + { + "tempEvoId": 1, + "attack": 342, + "defense": 239, + "stamina": 190, + "unreleased": true + }, + { + "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 +470,64 @@ "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 + }, + { + "tempEvoId": 1, + "attack": 262, + "defense": 232, + "stamina": 169, + "unreleased": true + }, + { + "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 From 1b1e81bcdc799487dd9459a3ae71357bd41feb06 Mon Sep 17 00:00:00 2001 From: Mygod Date: Mon, 15 Dec 2025 18:58:06 -0800 Subject: [PATCH 2/9] feat: mega z --- src/classes/PokeApi.ts | 6 ++++-- src/classes/Pokemon.ts | 14 +++++++++---- src/classes/Translations.ts | 13 ++++++++++++ static/baseStats.json | 8 ++++---- static/tempEvos.json | 40 +++++++++++++++++++++++++++++++++++++ 5 files changed, 71 insertions(+), 10 deletions(-) diff --git a/src/classes/PokeApi.ts b/src/classes/PokeApi.ts index cce241d..fa9ed7b 100644 --- a/src/classes/PokeApi.ts +++ b/src/classes/PokeApi.ts @@ -155,12 +155,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 } @@ -432,7 +434,7 @@ export default class PokeApi extends Masterfile { )) as { results?: BasePokeApiStruct[] } )?.results ?.map((pokemon) => pokemon.name) - ?.filter((name) => /-mega(?:-x|-y)?$/.test(name)) || [] + ?.filter((name) => /-mega(?:-[xyz])?$/.test(name)) || [] for (const [type, ids] of Object.entries(theoretical)) { this.tempEvos[type] = {} diff --git a/src/classes/Pokemon.ts b/src/classes/Pokemon.ts index 5ec15c7..a35f099 100644 --- a/src/classes/Pokemon.ts +++ b/src/classes/Pokemon.ts @@ -298,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: @@ -334,8 +338,10 @@ export default class Pokemon extends Masterfile { } return newTempEvolution }) - return tempEvolutions.sort( - (a, b) => (a.tempEvoId as number) - (b.tempEvoId as number), + return 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( diff --git a/src/classes/Translations.ts b/src/classes/Translations.ts index e2e2a72..2a9c063 100644 --- a/src/classes/Translations.ts +++ b/src/classes/Translations.ts @@ -927,6 +927,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/static/baseStats.json b/static/baseStats.json index 4dd36a0..d371f5c 100644 --- a/static/baseStats.json +++ b/static/baseStats.json @@ -13,7 +13,9 @@ "evoId": 899, "formId": 3218 } - ] + ], + "legendary": false, + "mythic": false }, "290": { "evolutions": [ @@ -21,9 +23,7 @@ "evoId": 292, "formId": 1439 } - ], - "legendary": false, - "mythic": false + ] }, "884": { "evolutions": [ diff --git a/static/tempEvos.json b/static/tempEvos.json index 711169e..8b1077d 100644 --- a/static/tempEvos.json +++ b/static/tempEvos.json @@ -144,6 +144,21 @@ } ] }, + "359": { + "tempEvolutions": [ + { + "tempEvoId": 5, + "attack": 332, + "defense": 138, + "stamina": 163, + "types": [ + 8, + 17 + ], + "unreleased": true + } + ] + }, "398": { "tempEvolutions": [ { @@ -159,6 +174,31 @@ } ] }, + "445": { + "tempEvolutions": [ + { + "tempEvoId": 5, + "attack": 321, + "defense": 196, + "stamina": 239, + "types": [ + 16 + ], + "unreleased": true + } + ] + }, + "448": { + "tempEvolutions": [ + { + "tempEvoId": 5, + "attack": 359, + "defense": 161, + "stamina": 172, + "unreleased": true + } + ] + }, "478": { "tempEvolutions": [ { From 694c5e6a016168fd60dda2b29f9b27e8b3c769aa Mon Sep 17 00:00:00 2001 From: Mygod Date: Mon, 15 Dec 2025 19:09:13 -0800 Subject: [PATCH 3/9] fix: dup mega --- src/classes/PokeApi.ts | 28 ++++++++++++++++++++++++++++ src/classes/Pokemon.ts | 22 +++++++++++++++++++++- static/tempEvos.json | 21 --------------------- 3 files changed, 49 insertions(+), 22 deletions(-) diff --git a/src/classes/PokeApi.ts b/src/classes/PokeApi.ts index fa9ed7b..f1680a5 100644 --- a/src/classes/PokeApi.ts +++ b/src/classes/PokeApi.ts @@ -507,6 +507,34 @@ export default class PokeApi extends Masterfile { 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.push(newTheoretical) this.tempEvos[type][pokemonId].tempEvolutions.sort((a, b) => typeof a.tempEvoId === 'number' && typeof b.tempEvoId === 'number' diff --git a/src/classes/Pokemon.ts b/src/classes/Pokemon.ts index a35f099..46b83ba 100644 --- a/src/classes/Pokemon.ts +++ b/src/classes/Pokemon.ts @@ -1170,12 +1170,32 @@ export default class Pokemon extends Masterfile { ) { Object.keys(tempEvos[category]).forEach((id) => { try { - const tempEvolutions = [ + const mergedTempEvolutions = [ ...tempEvos[category][id].tempEvolutions, ...(this.parsedPokemon[id].tempEvolutions ? this.parsedPokemon[id].tempEvolutions : []), ] + const tempEvolutions = Array.from( + new Map( + mergedTempEvolutions + .filter(Boolean) + .map((tempEvo) => [ + `${typeof tempEvo.tempEvoId}:${tempEvo.tempEvoId}`, + tempEvo, + ]), + ).values(), + ).sort((a, b) => + typeof a.tempEvoId === 'number' && typeof b.tempEvoId === 'number' + ? a.tempEvoId - b.tempEvoId + : typeof a.tempEvoId === 'number' + ? -1 + : typeof b.tempEvoId === 'number' + ? 1 + : a.tempEvoId + .toString() + .localeCompare(b.tempEvoId.toString()), + ) this.parsedPokemon[id] = { ...this.parsedPokemon[id], tempEvolutions, diff --git a/static/tempEvos.json b/static/tempEvos.json index 8b1077d..3f251b6 100644 --- a/static/tempEvos.json +++ b/static/tempEvos.json @@ -473,13 +473,6 @@ }, "801": { "tempEvolutions": [ - { - "tempEvoId": 1, - "attack": 342, - "defense": 239, - "stamina": 190, - "unreleased": true - }, { "tempEvoId": 1, "attack": 342, @@ -535,20 +528,6 @@ }, "978": { "tempEvolutions": [ - { - "tempEvoId": 1, - "attack": 262, - "defense": 232, - "stamina": 169, - "unreleased": true - }, - { - "tempEvoId": 1, - "attack": 262, - "defense": 232, - "stamina": 169, - "unreleased": true - }, { "tempEvoId": 1, "attack": 262, From fa5128e0adac4d2c2448501252653239285dc60b Mon Sep 17 00:00:00 2001 From: Mygod Date: Mon, 15 Dec 2025 19:53:08 -0800 Subject: [PATCH 4/9] fix: misc issues --- src/classes/PokeApi.ts | 263 +++++++++++++++-------------------------- src/classes/Pokemon.ts | 50 ++------ static/baseStats.json | 8 +- 3 files changed, 112 insertions(+), 209 deletions(-) diff --git a/src/classes/PokeApi.ts b/src/classes/PokeApi.ts index f1680a5..c91ecef 100644 --- a/src/classes/PokeApi.ts +++ b/src/classes/PokeApi.ts @@ -13,6 +13,7 @@ import { PokeApiTypes, } from '../typings/pokeapi' import { SpeciesApi } from '../typings/general' +import { sortTempEvolutions } from '../utils/tempEvolutions' export default class PokeApi extends Masterfile { baseStats: AllPokemon @@ -325,7 +326,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]) { @@ -350,7 +351,7 @@ export default class PokeApi extends Masterfile { 'Unable to find proto ID for', evoData.evolves_from_species.name .toUpperCase() - .replace('-', '_'), + .replace(/-/g, '_'), ) } } @@ -363,70 +364,6 @@ 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( @@ -436,117 +373,103 @@ export default class PokeApi extends Masterfile { ?.map((pokemon) => pokemon.name) ?.filter((name) => /-mega(?:-[xyz])?$/.test(name)) || [] - for (const [type, ids] of Object.entries(theoretical)) { - this.tempEvos[type] = {} - const combinedIds = Array.from( - new Set([...ids, ...(type === 'mega' ? discoveredMega : [])]), - ) - await Promise.all( - combinedIds.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: { [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 baseTypes = - parsedPokemon[pokemonId]?.types || this.baseStats[pokemonId]?.types - 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: - baseTypes && this.compare(types, baseTypes) ? undefined : types, - unreleased: true, - } - const alreadyExistsInGame = parsedPokemon[ - pokemonId - ]?.tempEvolutions?.some( - (temp) => temp.tempEvoId === newTheoretical.tempEvoId, + 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: { [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 + ], ) - if (alreadyExistsInGame) return + .sort((a, b) => a - b) - if (!this.tempEvos[type][pokemonId]) { - this.tempEvos[type][pokemonId] = {} - } - if (!this.tempEvos[type][pokemonId].tempEvolutions) { - this.tempEvos[type][pokemonId].tempEvolutions = [] - } + const baseTypes = + parsedPokemon[pokemonId]?.types || this.baseStats[pokemonId]?.types + 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: baseTypes && this.compare(types, baseTypes) ? undefined : types, + unreleased: true, + } + const alreadyExistsInGame = parsedPokemon[pokemonId]?.tempEvolutions?.some( + (temp) => temp.tempEvoId === newTheoretical.tempEvoId, + ) + if (alreadyExistsInGame) return - 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 (!this.tempEvos[type][pokemonId]) { + this.tempEvos[type][pokemonId] = {} + } + if (!this.tempEvos[type][pokemonId].tempEvolutions) { + this.tempEvos[type][pokemonId].tempEvolutions = [] + } - if (!existingTempEvolution.types && newTheoretical.types) { - existingTempEvolution.types = newTheoretical.types - } - return - } + 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 - this.tempEvos[type][pokemonId].tempEvolutions.push(newTheoretical) - 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}`) + if (!existingTempEvolution.types && newTheoretical.types) { + existingTempEvolution.types = newTheoretical.types + } + return } - }), - ) - } + + this.tempEvos[type][pokemonId].tempEvolutions.push(newTheoretical) + sortTempEvolutions(this.tempEvos[type][pokemonId].tempEvolutions) + } catch (e) { + console.warn(e, `Failed to parse PokeApi ${type} Evos for ${id}`) + } + }), + ) } async typesApi() { diff --git a/src/classes/Pokemon.ts b/src/classes/Pokemon.ts index 46b83ba..1996f94 100644 --- a/src/classes/Pokemon.ts +++ b/src/classes/Pokemon.ts @@ -16,6 +16,7 @@ import { EvolutionQuest, } from '../typings/general' import { Options } from '../typings/inputs' +import { mergeTempEvolutions, sortTempEvolutions } from '../utils/tempEvolutions' import { CostumeProto, FamilyProto, @@ -338,11 +339,7 @@ export default class Pokemon extends Masterfile { } return newTempEvolution }) - return tempEvolutions.sort((a, b) => - typeof a.tempEvoId === 'number' && typeof b.tempEvoId === 'number' - ? a.tempEvoId - b.tempEvoId - : a.tempEvoId.toString().localeCompare(b.tempEvoId.toString()), - ) + return sortTempEvolutions(tempEvolutions) } catch (e) { console.warn( e, @@ -1168,38 +1165,17 @@ export default class Pokemon extends Masterfile { this.options.includeEstimatedPokemon === true || this.options.includeEstimatedPokemon[category] ) { - Object.keys(tempEvos[category]).forEach((id) => { - try { - const mergedTempEvolutions = [ - ...tempEvos[category][id].tempEvolutions, - ...(this.parsedPokemon[id].tempEvolutions - ? this.parsedPokemon[id].tempEvolutions - : []), - ] - const tempEvolutions = Array.from( - new Map( - mergedTempEvolutions - .filter(Boolean) - .map((tempEvo) => [ - `${typeof tempEvo.tempEvoId}:${tempEvo.tempEvoId}`, - tempEvo, - ]), - ).values(), - ).sort((a, b) => - typeof a.tempEvoId === 'number' && typeof b.tempEvoId === 'number' - ? a.tempEvoId - b.tempEvoId - : typeof a.tempEvoId === 'number' - ? -1 - : typeof b.tempEvoId === 'number' - ? 1 - : a.tempEvoId - .toString() - .localeCompare(b.tempEvoId.toString()), - ) - this.parsedPokemon[id] = { - ...this.parsedPokemon[id], - tempEvolutions, - } + Object.keys(tempEvos[category]).forEach((id) => { + try { + const tempEvolutions = mergeTempEvolutions( + tempEvos[category][id].tempEvolutions, + this.parsedPokemon[id].tempEvolutions, + { prefer: 'actual' }, + ) + this.parsedPokemon[id] = { + ...this.parsedPokemon[id], + tempEvolutions, + } if (this.parsedPokemon[id].forms) { this.parsedPokemon[id].forms.forEach((form) => { if ( diff --git a/static/baseStats.json b/static/baseStats.json index d371f5c..4f32a32 100644 --- a/static/baseStats.json +++ b/static/baseStats.json @@ -23,7 +23,9 @@ "evoId": 292, "formId": 1439 } - ] + ], + "legendary": false, + "mythic": false }, "884": { "evolutions": [ @@ -31,7 +33,9 @@ "evoId": 1018, "formId": 3330 } - ] + ], + "legendary": false, + "mythic": false }, "902": { "pokemonName": "Basculegion-male", From f8ae692b592d1ef33336ea8acb499189f7c4e7b7 Mon Sep 17 00:00:00 2001 From: Mygod Date: Mon, 15 Dec 2025 20:05:11 -0800 Subject: [PATCH 5/9] fix: missing files --- src/utils/tempEvolutions.ts | 57 +++++++++++++++++++++++++++++++++++++ tests/tempEvos.test.js | 18 ++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 src/utils/tempEvolutions.ts create mode 100644 tests/tempEvos.test.js diff --git a/src/utils/tempEvolutions.ts b/src/utils/tempEvolutions.ts new file mode 100644 index 0000000..c1e1f46 --- /dev/null +++ b/src/utils/tempEvolutions.ts @@ -0,0 +1,57 @@ +import { TempEvolutions } from '../typings/dataTypes' + +export type TempEvoId = TempEvolutions['tempEvoId'] + +export const tempEvoIdKey = (tempEvoId: TempEvoId): string => + `${typeof tempEvoId}:${tempEvoId}` + +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()) +} + +export const sortTempEvolutions = ( + tempEvolutions: TempEvolutions[], +): TempEvolutions[] => + tempEvolutions.sort((a, b) => compareTempEvoId(a.tempEvoId, b.tempEvoId)) + +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())) +} + +export const mergeTempEvolutions = ( + estimated: (TempEvolutions | undefined | null)[] | undefined, + actual: (TempEvolutions | undefined | null)[] | undefined, + options: { prefer?: 'estimated' | 'actual' } = {}, +): TempEvolutions[] => { + const prefer = options.prefer ?? 'actual' + const estimatedList = Array.isArray(estimated) ? estimated : [] + const actualList = Array.isArray(actual) ? actual : [] + + return prefer === 'actual' + ? dedupeTempEvolutions([...estimatedList, ...actualList], { prefer: 'last' }) + : dedupeTempEvolutions([...actualList, ...estimatedList], { prefer: 'last' }) +} diff --git a/tests/tempEvos.test.js b/tests/tempEvos.test.js new file mode 100644 index 0000000..0e3023b --- /dev/null +++ b/tests/tempEvos.test.js @@ -0,0 +1,18 @@ +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 = `${typeof evo.tempEvoId}:${evo.tempEvoId}` + expect(seen.has(key)).toBe(false) + seen.add(key) + }) + }) + }) + }) +}) + From 575e83dcc68f0e824e5fb9d4cf9c72ba70f56498 Mon Sep 17 00:00:00 2001 From: Mygod Date: Mon, 15 Dec 2025 20:14:52 -0800 Subject: [PATCH 6/9] chore: reduce code dup --- src/classes/PokeApi.ts | 129 ++++++++++++++++------------------------- 1 file changed, 51 insertions(+), 78 deletions(-) diff --git a/src/classes/PokeApi.ts b/src/classes/PokeApi.ts index c91ecef..c9a90fa 100644 --- a/src/classes/PokeApi.ts +++ b/src/classes/PokeApi.ts @@ -110,6 +110,46 @@ 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 mapTypeIds(types: PokeApiStats['types']): number[] { + return types + .map( + (type) => + Rpc.HoloPokemonType[ + `POKEMON_TYPE_${type.type.name.toUpperCase()}` as TypeProto + ], + ) + .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, @@ -210,59 +250,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 +291,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) { @@ -397,34 +387,17 @@ export default class PokeApi extends Masterfile { console.warn('Unable to resolve Pokemon ID for temp evo', id) return } - 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 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: PokeApi.attack( - baseStats.attack, - baseStats['special-attack'], - baseStats.speed, - ), - defense: PokeApi.defense( - baseStats.defense, - baseStats['special-defense'], - baseStats.speed, - ), - stamina: PokeApi.stamina(baseStats.hp), + attack: computedStats.attack, + defense: computedStats.defense, + stamina: computedStats.stamina, types: baseTypes && this.compare(types, baseTypes) ? undefined : types, unreleased: true, } From b31a843b967b7004f0576c4e3c87d20878c8c375 Mon Sep 17 00:00:00 2001 From: Mygod Date: Mon, 15 Dec 2025 20:55:51 -0800 Subject: [PATCH 7/9] fix: more dup --- src/classes/PokeApi.ts | 39 ++++++++++++++++--------------------- src/classes/Pokemon.ts | 21 ++++++++++---------- src/utils/tempEvolutions.ts | 6 +----- 3 files changed, 28 insertions(+), 38 deletions(-) diff --git a/src/classes/PokeApi.ts b/src/classes/PokeApi.ts index c9a90fa..aad5938 100644 --- a/src/classes/PokeApi.ts +++ b/src/classes/PokeApi.ts @@ -118,14 +118,19 @@ export default class PokeApi extends Masterfile { 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) => - Rpc.HoloPokemonType[ - `POKEMON_TYPE_${type.type.name.toUpperCase()}` as TypeProto - ], - ) + .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) } @@ -446,16 +451,6 @@ export default class PokeApi extends Masterfile { } 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 { @@ -476,12 +471,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 1996f94..de3d0fa 100644 --- a/src/classes/Pokemon.ts +++ b/src/classes/Pokemon.ts @@ -1165,17 +1165,16 @@ export default class Pokemon extends Masterfile { this.options.includeEstimatedPokemon === true || this.options.includeEstimatedPokemon[category] ) { - Object.keys(tempEvos[category]).forEach((id) => { - try { - const tempEvolutions = mergeTempEvolutions( - tempEvos[category][id].tempEvolutions, - this.parsedPokemon[id].tempEvolutions, - { prefer: 'actual' }, - ) - this.parsedPokemon[id] = { - ...this.parsedPokemon[id], - tempEvolutions, - } + Object.keys(tempEvos[category]).forEach((id) => { + try { + const tempEvolutions = mergeTempEvolutions( + tempEvos[category][id].tempEvolutions, + this.parsedPokemon[id].tempEvolutions, + ) + this.parsedPokemon[id] = { + ...this.parsedPokemon[id], + tempEvolutions, + } if (this.parsedPokemon[id].forms) { this.parsedPokemon[id].forms.forEach((form) => { if ( diff --git a/src/utils/tempEvolutions.ts b/src/utils/tempEvolutions.ts index c1e1f46..e9f9854 100644 --- a/src/utils/tempEvolutions.ts +++ b/src/utils/tempEvolutions.ts @@ -45,13 +45,9 @@ export const dedupeTempEvolutions = ( export const mergeTempEvolutions = ( estimated: (TempEvolutions | undefined | null)[] | undefined, actual: (TempEvolutions | undefined | null)[] | undefined, - options: { prefer?: 'estimated' | 'actual' } = {}, ): TempEvolutions[] => { - const prefer = options.prefer ?? 'actual' const estimatedList = Array.isArray(estimated) ? estimated : [] const actualList = Array.isArray(actual) ? actual : [] - return prefer === 'actual' - ? dedupeTempEvolutions([...estimatedList, ...actualList], { prefer: 'last' }) - : dedupeTempEvolutions([...actualList, ...estimatedList], { prefer: 'last' }) + return dedupeTempEvolutions([...estimatedList, ...actualList], { prefer: 'last' }) } From 32e1be85ff709ad06001dfc05164fd1caa1760c9 Mon Sep 17 00:00:00 2001 From: Mygod Date: Mon, 15 Dec 2025 21:40:46 -0800 Subject: [PATCH 8/9] refactor: make temp evo utils safer --- src/classes/PokeApi.ts | 6 ++- src/utils/tempEvolutions.ts | 24 ++++++++++- tests/tempEvolutions.utils.test.js | 64 ++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 tests/tempEvolutions.utils.test.js diff --git a/src/classes/PokeApi.ts b/src/classes/PokeApi.ts index 2db84ae..b65a2c6 100644 --- a/src/classes/PokeApi.ts +++ b/src/classes/PokeApi.ts @@ -444,8 +444,10 @@ export default class PokeApi extends Masterfile { return } - this.tempEvos[type][pokemonId].tempEvolutions.push(newTheoretical) - sortTempEvolutions(this.tempEvos[type][pokemonId].tempEvolutions) + 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}`) } diff --git a/src/utils/tempEvolutions.ts b/src/utils/tempEvolutions.ts index e9f9854..faedfdd 100644 --- a/src/utils/tempEvolutions.ts +++ b/src/utils/tempEvolutions.ts @@ -1,10 +1,20 @@ -import { TempEvolutions } from '../typings/dataTypes' +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' @@ -15,11 +25,18 @@ export const compareTempEvoId = (a: TempEvoId, b: TempEvoId): number => { 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)) + [...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' } = {}, @@ -42,6 +59,9 @@ export const dedupeTempEvolutions = ( 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, 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], + ]) + }) +}) + From 355cc4b25b07123e8cf29e383a40c12cc09d7d48 Mon Sep 17 00:00:00 2001 From: Mygod Date: Mon, 15 Dec 2025 22:56:58 -0800 Subject: [PATCH 9/9] fix: test dup code --- tests/tempEvos.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/tempEvos.test.js b/tests/tempEvos.test.js index 0e3023b..4a3903a 100644 --- a/tests/tempEvos.test.js +++ b/tests/tempEvos.test.js @@ -1,3 +1,4 @@ +const { tempEvoIdKey } = require('../dist/utils/tempEvolutions') const tempEvos = require('../static/tempEvos.json') describe('static/tempEvos.json', () => { @@ -7,7 +8,7 @@ describe('static/tempEvos.json', () => { const seen = new Set() const evolutions = (data && data.tempEvolutions) || [] evolutions.forEach((evo) => { - const key = `${typeof evo.tempEvoId}:${evo.tempEvoId}` + const key = tempEvoIdKey(evo.tempEvoId) expect(seen.has(key)).toBe(false) seen.add(key) }) @@ -15,4 +16,3 @@ describe('static/tempEvos.json', () => { }) }) }) -