diff --git a/server/src/filters/builder/pokemon.js b/server/src/filters/builder/pokemon.js index 6a2fc5c58..6e8518859 100644 --- a/server/src/filters/builder/pokemon.js +++ b/server/src/filters/builder/pokemon.js @@ -1,6 +1,7 @@ // @ts-check const { state } = require('../../services/state') const { BaseFilter } = require('../Base') +const { getWildFilterKey } = require('../pokemon/getWildFilterKey') /** * @@ -36,20 +37,18 @@ function buildPokemon(defaults, base, custom) { Object.entries(state.event.masterfile.pokemon).forEach(([id, pkmn]) => { pokemon.quests[`${id}`] = new BaseFilter(defaults.pokestops.pokemon) Object.keys(pkmn.forms).forEach((form) => { - pokemon.full[`${id}-${form}`] = base - pokemon.raids[`${id}-${form}`] = new BaseFilter(defaults.gyms.pokemon) - pokemon.stations[`${id}-${form}`] = new BaseFilter( - defaults.stations.pokemon, - ) - pokemon.quests[`${id}-${form}`] = new BaseFilter( - defaults.pokestops.pokemon, - ) + const filterKey = getWildFilterKey(id, form) + const rawKey = `${id}-${form}` + pokemon.full[filterKey] = base + pokemon.raids[rawKey] = new BaseFilter(defaults.gyms.pokemon) + pokemon.stations[rawKey] = new BaseFilter(defaults.stations.pokemon) + pokemon.quests[rawKey] = new BaseFilter(defaults.pokestops.pokemon) if (state.db.filterContext.Pokestop.hasConfirmedInvasions) { - pokemon.rocket[`a${id}-${form}`] = new BaseFilter( + pokemon.rocket[`a${rawKey}`] = new BaseFilter( defaults.pokestops.invasionPokemon, ) } - pokemon.nests[`${id}-${form}`] = new BaseFilter(defaults.nests.allPokemon) + pokemon.nests[rawKey] = new BaseFilter(defaults.nests.allPokemon) }) if ('family' in pkmn) { if (pkmn.family === +id) { diff --git a/server/src/filters/pokemon/Backend.js b/server/src/filters/pokemon/Backend.js index 6c11ef166..77c21d12e 100644 --- a/server/src/filters/pokemon/Backend.js +++ b/server/src/filters/pokemon/Backend.js @@ -4,6 +4,7 @@ const config = require('@rm/config') const { log, TAGS } = require('@rm/logger') const { AND_KEYS, BASE_KEYS } = require('./constants') +const { getWildFilterKey } = require('./getWildFilterKey') const { deepCompare, between, @@ -372,7 +373,7 @@ class PkmnBackend { return true if ( !this.mods.onlyLinkGlobal || - (this.pokemon === pokemon.pokemon_id && this.form === pokemon.form) + this.id === getWildFilterKey(pokemon.pokemon_id, pokemon.form) ) { if (!this.expertFilter || !this.expertGlobal) return true if (this.expertFilter(pokemon)) { diff --git a/server/src/filters/pokemon/getWildFilterKey.js b/server/src/filters/pokemon/getWildFilterKey.js new file mode 100644 index 000000000..a70ac58b6 --- /dev/null +++ b/server/src/filters/pokemon/getWildFilterKey.js @@ -0,0 +1,25 @@ +const DITTO_ID = 132 + +const normalizePokemonId = (pokemonId) => { + const parsedPokemonId = Number.parseInt(`${pokemonId}`, 10) + return Number.isNaN(parsedPokemonId) ? 0 : parsedPokemonId +} + +const normalizePokemonForm = (pokemonId, formId = 0) => { + if (normalizePokemonId(pokemonId) === DITTO_ID) { + // Confirmed wild Ditto is already treated as species-based upstream. + // Golbat/MEM keeps the scanner lookup on `pokemon_id = 132, form = 0`, + // while the raw form field may still carry the disguise form for display. + return 0 + } + const parsedFormId = Number.parseInt(`${formId ?? 0}`, 10) + return Number.isNaN(parsedFormId) ? 0 : parsedFormId +} + +const getWildFilterKey = (pokemonId, formId = 0) => + `${normalizePokemonId(pokemonId)}-${normalizePokemonForm(pokemonId, formId)}` + +module.exports = { + DITTO_ID, + getWildFilterKey, +} diff --git a/server/src/models/Pokemon.js b/server/src/models/Pokemon.js index 8fed505b7..f9dfbc46d 100644 --- a/server/src/models/Pokemon.js +++ b/server/src/models/Pokemon.js @@ -25,13 +25,12 @@ const { BASE_KEYS, } = require('../filters/pokemon/constants') const { PkmnBackend } = require('../filters/pokemon/Backend') +const { + DITTO_ID, + getWildFilterKey, +} = require('../filters/pokemon/getWildFilterKey') const { state } = require('../services/state') -const DITTO_ID = 132 - -const getPokemonFilterKey = (pokemonId, form) => - pokemonId === DITTO_ID ? `${DITTO_ID}-0` : `${pokemonId}-${form}` - class Pokemon extends Model { static get tableName() { return 'pokemon' @@ -169,7 +168,12 @@ class Pokemon extends Model { const pokemonForms = [] Object.values(filterMap).forEach((filter) => { pokemonIds.push(filter.pokemon) - pokemonForms.push(filter.form) + if (!(filter.pokemon === DITTO_ID && filter.form === 0)) { + // Wild Ditto uses a synthetic canonical filter form. Keep the SQL + // prefilter species-only here so disguise form ids don't get turned + // into `form = 0` broad matches. + pokemonForms.push(filter.form) + } if ( !queryPvp && config @@ -260,8 +264,12 @@ class Pokemon extends Model { } else { ivOr.whereNull('pokemon_id') } - ivOr.orWhereIn('pokemon_id', pokemonIds) - ivOr.orWhereIn('pokemon.form', pokemonForms) + if (pokemonIds.length) { + ivOr.orWhereIn('pokemon_id', pokemonIds) + } + if (pokemonForms.length) { + ivOr.orWhereIn('pokemon.form', pokemonForms) + } } if (onlyZeroIv && ivs) { ivOr.orWhere(isMad ? raw(IV_CALC) : 'iv', 0) @@ -359,7 +367,7 @@ class Pokemon extends Model { // form checker for (let i = 0; i < results.length; i += 1) { const pkmn = results[i] - const id = getPokemonFilterKey(pkmn.pokemon_id, pkmn.form) + const id = getWildFilterKey(pkmn.pokemon_id, pkmn.form) const filter = filterMap[id] || globalFilter let noPvp = true @@ -439,8 +447,7 @@ class Pokemon extends Model { for (let i = 0; i < pvpResults.length; i += 1) { const pkmn = pvpResults[i] const filter = - filterMap[getPokemonFilterKey(pkmn.pokemon_id, pkmn.form)] || - globalFilter + filterMap[getWildFilterKey(pkmn.pokemon_id, pkmn.form)] || globalFilter const result = filter.build(pkmn) if (filter.valid(result)) { finalResults.push(result) @@ -803,13 +810,13 @@ class Pokemon extends Model { const built = filtered .map((item) => { const filter = - filterMap[getPokemonFilterKey(item.pokemon_id, item.form)] || + filterMap[getWildFilterKey(item.pokemon_id, item.form)] || globalFilter return filter.build(item) }) .filter((pkmn) => { const filter = - filterMap[getPokemonFilterKey(pkmn.pokemon_id, pkmn.form)] || + filterMap[getWildFilterKey(pkmn.pokemon_id, pkmn.form)] || globalFilter return filter.valid(pkmn) }) @@ -886,14 +893,22 @@ class Pokemon extends Model { secret, httpAuth, ) - available.forEach((pkmn) => { - if (pkmn.id === DITTO_ID) pkmn.form = 0 - }) + const normalizedAvailable = available.reduce( + (acc, pkmn) => { + // Wild Ditto reports disguise form ids here, not true Ditto form ids. + // Normalize them to a single wild-filter key for the Pokémon drawer. + const key = getWildFilterKey(pkmn.id, pkmn.form) + acc.available.add(key) + const current = Number(acc.rarity.get(key) ?? 0) + const count = Number(pkmn.count ?? 0) + acc.rarity.set(key, current + count) + return acc + }, + { available: new Set(), rarity: new Map() }, + ) return { - available: available.map((pkmn) => `${pkmn.id}-${pkmn.form}`), - rarity: Object.fromEntries( - available.map((pkmn) => [`${pkmn.id}-${pkmn.form}`, pkmn.count]), - ), + available: [...normalizedAvailable.available], + rarity: Object.fromEntries(normalizedAvailable.rarity), } } diff --git a/server/src/services/EventManager.js b/server/src/services/EventManager.js index 1eb5091bb..a6538db11 100644 --- a/server/src/services/EventManager.js +++ b/server/src/services/EventManager.js @@ -415,6 +415,11 @@ class EventManager extends Logger { if (!Number.isNaN(parseInt(item.charAt(0)))) { const [id, form] = item.split('-') const formId = form || '0' + if (category === 'pokemon' && id === '132' && formId === '0') { + // Wild Ditto uses a synthetic filter key here. Do not backfill it + // into the masterfile as a real form entry. + return + } if (!this.masterfile.pokemon[id]) { this.masterfile.pokemon[id] = { name: '', diff --git a/src/features/pokemon/PokemonPopup.jsx b/src/features/pokemon/PokemonPopup.jsx index f88937713..4fedc3f44 100644 --- a/src/features/pokemon/PokemonPopup.jsx +++ b/src/features/pokemon/PokemonPopup.jsx @@ -33,6 +33,7 @@ import { GET_TAPPABLE_BY_ID } from '@services/queries/tappable' import { usePokemonBackgroundVisual } from '@hooks/usePokemonBackgroundVisuals' import { BackgroundCard } from '@components/popups/BackgroundCard' import { getFormDisplay } from '@utils/getFormDisplay' +import { getWildFilterId } from '@utils/getWildFilterId' const rowClass = { width: 30, fontWeight: 'bold' } @@ -346,6 +347,7 @@ const Header = ({ pokemon, metaData, iconUrl, userSettings, isTutorial }) => { const [anchorEl, setAnchorEl] = React.useState(null) const { id, pokemon_id, form, display_pokemon_id } = pokemon + const filterKey = getWildFilterId(pokemon_id, form) const handleClick = (event) => { setAnchorEl(event.currentTarget) @@ -363,8 +365,7 @@ const Header = ({ pokemon, metaData, iconUrl, userSettings, isTutorial }) => { const handleExclude = () => { setAnchorEl(null) if (filters?.pokemon?.filter) { - const key = `${pokemon_id}-${form}` - setDeepStore(`filters.pokemon.filter.${key}.enabled`, false) + setDeepStore(`filters.pokemon.filter.${filterKey}.enabled`, false) } } @@ -382,10 +383,7 @@ const Header = ({ pokemon, metaData, iconUrl, userSettings, isTutorial }) => { { name: 'timer', action: handleTimer }, { name: 'hide', action: handleHide }, ] - if ( - isTutorial || - filters?.pokemon?.filter?.[`${pokemon_id}-${form}`]?.enabled - ) { + if (isTutorial || filters?.pokemon?.filter?.[filterKey]?.enabled) { options.push({ name: 'exclude', action: handleExclude }) } const pokeName = t(`poke_${metaData.pokedexId}`) diff --git a/src/features/pokemon/PokemonTile.jsx b/src/features/pokemon/PokemonTile.jsx index 29f4ce638..f2e288872 100644 --- a/src/features/pokemon/PokemonTile.jsx +++ b/src/features/pokemon/PokemonTile.jsx @@ -20,6 +20,7 @@ import { getTimeUntil } from '@utils/getTimeUntil' import { normalizeCategory } from '@utils/normalizeCategory' import { getS2Polygon } from '@utils/getS2Polygon' import { getFormDisplay } from '@utils/getFormDisplay' +import { getWildFilterId } from '@utils/getWildFilterId' import { PokemonPopup } from './PokemonPopup' import { basicPokemonMarker, fancyPokemonMarker } from './pokemonMarker' @@ -46,7 +47,7 @@ const getGlowStatus = (pkmn, userSettings) => { * @returns */ const BasePokemonTile = (pkmn) => { - const internalId = `${pkmn.pokemon_id}-${pkmn.form}` + const internalId = getWildFilterId(pkmn.pokemon_id, pkmn.form) const [markerRef, setMarkerRef] = React.useState(null) diff --git a/src/pages/map/hooks/useGenPokemon.js b/src/pages/map/hooks/useGenPokemon.js index 2968ab9d8..8a457cd6a 100644 --- a/src/pages/map/hooks/useGenPokemon.js +++ b/src/pages/map/hooks/useGenPokemon.js @@ -3,6 +3,7 @@ import { useEffect } from 'react' import { useTranslation } from 'react-i18next' import { useMemory } from '@store/useMemory' +import { getWildFilterId } from '@utils/getWildFilterId' export function useGenPokemon() { const { t } = useTranslation() @@ -29,7 +30,10 @@ export function useGenPokemon() { const pokeName = t(`poke_${i}`) Object.entries(pkmn.forms).forEach(([j, form]) => { const formName = t(`form_${j}`) - const id = `${i}-${j}` + const id = getWildFilterId(i, j) + if (tempObj.pokemon[id]) { + return + } const formTypes = (form.types || pkmn.types || []).map( (x) => `poke_type_${x}`, ) diff --git a/src/utils/getWildFilterId.js b/src/utils/getWildFilterId.js new file mode 100644 index 000000000..5851111c4 --- /dev/null +++ b/src/utils/getWildFilterId.js @@ -0,0 +1,12 @@ +const DITTO_ID = 132 + +export const getWildFilterId = (pokemonId, formId = 0) => { + const normalizedPokemonId = Number.parseInt(`${pokemonId}`, 10) + const normalizedFormId = Number.parseInt(`${formId ?? 0}`, 10) + if (normalizedPokemonId === DITTO_ID) { + return `${DITTO_ID}-0` + } + return `${Number.isNaN(normalizedPokemonId) ? 0 : normalizedPokemonId}-${ + Number.isNaN(normalizedFormId) ? 0 : normalizedFormId + }` +}