From 3c4ef00d12537a0f2f8ec1334ef2e7ca4dcb7839 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Tue, 9 Aug 2022 21:04:09 -0700 Subject: [PATCH 001/143] feat(calcdex): darked some colors --- .../layout/TableGrid/TableGrid.module.scss | 4 ++-- src/pages/Calcdex/Calcdex.module.scss | 2 +- src/pages/Calcdex/PlayerCalc.module.scss | 14 ++++++++------ src/pages/Calcdex/PokeInfo.module.scss | 8 ++++---- src/pages/Calcdex/PokeMoves.module.scss | 16 ++++++++++------ 5 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/components/layout/TableGrid/TableGrid.module.scss b/src/components/layout/TableGrid/TableGrid.module.scss index 6ed7a30b..ae8d005a 100644 --- a/src/components/layout/TableGrid/TableGrid.module.scss +++ b/src/components/layout/TableGrid/TableGrid.module.scss @@ -28,10 +28,10 @@ user-select: none; &.light { - color: color.alpha(colors.$black, 0.65); + color: color.alpha(colors.$black, 0.75); } &.dark { - color: color.alpha(colors.$white, 0.65); + color: color.alpha(colors.$white, 0.75); } } diff --git a/src/pages/Calcdex/Calcdex.module.scss b/src/pages/Calcdex/Calcdex.module.scss index 5add31fa..e62f1450 100644 --- a/src/pages/Calcdex/Calcdex.module.scss +++ b/src/pages/Calcdex/Calcdex.module.scss @@ -25,7 +25,7 @@ } &.light::before { - background-color: color.alpha(colors.$white, 0.5); + background-color: color.alpha(colors.$white, 0.4); } &.dark::before { diff --git a/src/pages/Calcdex/PlayerCalc.module.scss b/src/pages/Calcdex/PlayerCalc.module.scss index c67816c0..0e7d88b9 100644 --- a/src/pages/Calcdex/PlayerCalc.module.scss +++ b/src/pages/Calcdex/PlayerCalc.module.scss @@ -14,13 +14,15 @@ width: 100%; } -// .playerInfo { -// @include flex.column; -// } +.playerInfo { + // @include flex.column; + width: 100%; +} button.usernameButton { justify-content: initial; - max-width: 78px; + // max-width: 78px; + max-width: 80%; } .usernameButtonLabel { @@ -30,11 +32,11 @@ button.usernameButton { overflow: hidden; .light & { - color: color.alpha(colors.$black, 0.5); + color: color.alpha(colors.$black, 0.75); } .dark & { - color: color.alpha(colors.$white, 0.5); + color: color.alpha(colors.$white, 0.75); } } diff --git a/src/pages/Calcdex/PokeInfo.module.scss b/src/pages/Calcdex/PokeInfo.module.scss index e3af9173..e2b594c2 100644 --- a/src/pages/Calcdex/PokeInfo.module.scss +++ b/src/pages/Calcdex/PokeInfo.module.scss @@ -39,11 +39,11 @@ } .light & { - color: color.alpha(colors.$black, 0.65); + color: color.alpha(colors.$black, 0.75); } .dark & { - color: color.alpha(colors.$white, 0.65); + color: color.alpha(colors.$white, 0.75); } } @@ -63,11 +63,11 @@ .currentHp { .light & { - color: color.alpha(colors.$black, 0.65); + color: color.alpha(colors.$black, 0.75); } .dark & { - color: color.alpha(colors.$white, 0.65); + color: color.alpha(colors.$white, 0.75); } } diff --git a/src/pages/Calcdex/PokeMoves.module.scss b/src/pages/Calcdex/PokeMoves.module.scss index 0346ba9e..cc5fd9c5 100644 --- a/src/pages/Calcdex/PokeMoves.module.scss +++ b/src/pages/Calcdex/PokeMoves.module.scss @@ -13,15 +13,19 @@ &.light { background-color: color.alpha(colors.$white, 0.6); - box-shadow: 0 0 1px color.alpha(colors.$black, 0.15), - 0 0 3px color.alpha(colors.$black, 0.15); + box-shadow: ( + 0 0 1px color.alpha(colors.$black, 0.15), + 0 0 3px color.alpha(colors.$black, 0.15), + ); } &.dark { background-color: color.alpha(colors.$black, 0.75); // box-shadow: 0 0 1px color.alpha(colors.$white, 0.15); - box-shadow: 0 0 1px color.alpha(colors.$black, 0.15), - 0 0 5px color.alpha(colors.$black, 0.15); + box-shadow: ( + 0 0 1px color.alpha(colors.$black, 0.15), + 0 0 5px color.alpha(colors.$black, 0.15), + ); } } @@ -39,11 +43,11 @@ user-select: none; .light & { - color: color.alpha(colors.$black, 0.65); + color: color.alpha(colors.$black, 0.75); } .dark & { - color: color.alpha(colors.$white, 0.65); + color: color.alpha(colors.$white, 0.75); } } From 7f8a18a352ab15b9b2c5858d8e2bde5309f2b6f4 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Fri, 12 Aug 2022 22:59:22 -0700 Subject: [PATCH 002/143] feat(redux): installed and configured redux & RTK Query --- package.json | 3 + src/redux/actions/index.ts | 1 + src/redux/actions/syncBattle.ts | 363 ++++++++++++ src/redux/services/index.ts | 2 + src/redux/services/pkmnApi.ts | 33 ++ src/redux/services/presetApi.ts | 231 ++++++++ src/redux/store/calcdexSlice.ts | 938 ++++++++++++++++++++++++++++++++ src/redux/store/createStore.ts | 70 +++ src/redux/store/hooks.ts | 20 + src/redux/store/index.ts | 3 + yarn.lock | 78 ++- 11 files changed, 1740 insertions(+), 2 deletions(-) create mode 100644 src/redux/actions/index.ts create mode 100644 src/redux/actions/syncBattle.ts create mode 100644 src/redux/services/index.ts create mode 100644 src/redux/services/pkmnApi.ts create mode 100644 src/redux/services/presetApi.ts create mode 100644 src/redux/store/calcdexSlice.ts create mode 100644 src/redux/store/createStore.ts create mode 100644 src/redux/store/hooks.ts create mode 100644 src/redux/store/index.ts diff --git a/package.json b/package.json index dd2bea47..f8c6e3d0 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@react-aria/textfield": "^3.7.0", "@react-aria/visually-hidden": "^3.4.0", "@react-spring/web": "^9.5.2", + "@reduxjs/toolkit": "^1.8.3", "@smogon/calc": "^0.6.0", "@tippyjs/react": "^4.2.6", "@use-gesture/react": "^10.2.17", @@ -57,10 +58,12 @@ "date-fns": "^2.29.1", "dotenv": "^16.0.1", "final-form": "^4.20.7", + "qs": "^6.11.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-final-form": "^6.5.9", "react-hot-loader": "^4.13.0", + "react-redux": "^8.0.2", "react-select": "^5.4.0", "remove": "^0.1.5", "slugify": "^1.6.5", diff --git a/src/redux/actions/index.ts b/src/redux/actions/index.ts new file mode 100644 index 00000000..e947f05d --- /dev/null +++ b/src/redux/actions/index.ts @@ -0,0 +1 @@ +export * from './syncBattle'; diff --git a/src/redux/actions/syncBattle.ts b/src/redux/actions/syncBattle.ts new file mode 100644 index 00000000..45abd218 --- /dev/null +++ b/src/redux/actions/syncBattle.ts @@ -0,0 +1,363 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { + detectPlayerKeyFromBattle, + sanitizePokemon, + syncField, + syncPokemon, +} from '@showdex/utils/battle'; +import { calcPokemonCalcdexId } from '@showdex/utils/calc'; +import { env } from '@showdex/utils/core'; +import { logger } from '@showdex/utils/debug'; +import type { Generation } from '@pkmn/data'; +import type { CalcdexBattleState, CalcdexPlayerKey, CalcdexSliceState } from '@showdex/redux/store'; + +const l = logger('@showdex/redux/actions/syncBattle'); + +export interface SyncBattlePayload { + battle: Showdown.Battle; + dex: Generation; +} + +export const SyncBattleActionType = 'calcdex:sync'; + +/* eslint-disable no-nested-ternary */ + +/** + * Syncs the Showdown `battle` state with an existing `CalcdexBattleState`. + * + * @since 0.1.3 + */ +export const syncBattle = createAsyncThunk( + SyncBattleActionType, + + async (payload, api) => { + l.debug( + 'RECV', SyncBattleActionType, + '\n', 'payload', payload, + ); + + const { + battle, + dex, + } = payload || {}; + + const rootState = > api.getState(); + const { calcdex: state } = rootState; + + // l.debug('calcdex state', state); + + const { + id: battleId, + nonce: battleNonce, + myPokemon, + } = battle || {}; + + if (!battleId) { + throw new Error('Attempted to initialize a CalcdexBattleState with a falsy battleId.'); + } + + if (!(battleId in state)) { + throw new Error(`Could not find a CalcdexBattleState with battleId ${battleId}`); + } + + // state isn't actually mutable here, so this is a pretty performant way to deep-copy + // since we don't use any complex data types, just primitives + const battleState = JSON.parse(JSON.stringify(state[battleId])); + + // l.debug( + // '\n', 'pre-copied battleState', state[battleId], + // '\n', 'deep-copied battleState', battleState, + // ); + + if (battleState.battleNonce && battleState.battleNonce === battleNonce) { + if (__DEV__) { + l.debug( + 'Skipping this round of syncing due to same battleNonce from before', + '\n', 'battleNonce', battleNonce, + '\n', 'battle', battle, + '\n', 'battleState', battleState, + '\n', '(You will only see this message on development.)', + ); + } + + return; + } + + if (typeof dex?.learnsets?.learnable !== 'function') { + throw new Error('Missing required dex property in payload argument.'); + } + + // find out which side myPokemon belongs to + const myPokemonSide = detectPlayerKeyFromBattle(battle); + + for (const playerKey of ['p1', 'p2']) { + // l.debug('Processing player', playerKey); + + if (!(playerKey in battle) || battle[playerKey]?.sideid !== playerKey) { + if (__DEV__) { + l.warn( + 'Ignoring player updates for', playerKey, 'since it doesn\'t exist in the battle state.', + '\n', `battle.${playerKey}`, battle[playerKey], + '\n', '(You will only see this warning on development.)', + ); + } + + continue; + } + + const player = battle[playerKey]; + const playerState = battleState[playerKey]; + + if (player.name && playerState.name !== player.name) { + playerState.name = player.name; + } + + if (player.rating && playerState.rating !== player.rating) { + playerState.rating = player.rating; + } + + // l.debug( + // 'name/rating update', + // '\n', 'player', player, + // '\n', 'playerState', playerState, + // '\n', '__DEV__', __DEV__, + // ); + + if (!Array.isArray(player.pokemon) || !player.pokemon.length) { + if (__DEV__) { + l.warn( + 'Ignoring Pokemon updates for', playerKey, 'since they don\'t have any pokemon.', + '\n', 'player.pokemon', player.pokemon, + '\n', 'playerState.pokemon', playerState.pokemon, + '\n', '(You will only see this warning on development.)', + ); + } + + continue; + } + + // determine if `myPokemon` belongs to the current player + const isMyPokemonSide = !!myPokemonSide && + playerKey === myPokemonSide && + Array.isArray(myPokemon) && + !!myPokemon.length; + + // const hasUnrevealed = isMyPokemonSide && + // myPokemon.length > player.pokemon.length; + + // preserve the initial ordering of myPokemon since it's subject to change its indices + // (battle state may move the most recent active Pokemon to the front of the array) + if (isMyPokemonSide && !playerState.pokemonOrder?.length) { + playerState.pokemonOrder = myPokemon.map((p) => p.searchid); + } + + // reconstruct a full list of the current player's Pokemon, whether revealed or not + // (but if we don't have the relevant info [i.e., !isMyPokemonSide], then just access the player's `pokemon`) + const playerPokemon = isMyPokemonSide ? playerState.pokemonOrder.map((searchid) => { + const serverPokemon = myPokemon.find((p) => p.searchid === searchid); + + // try to find a matching clientPokemon that has already been revealed using the searchid, + // which is seemingly consistent between the player's `pokemon` (Pokemon[]) and `myPokemon` (ServerPokemon[]) + const clientPokemonIndex = player.pokemon.findIndex((p) => p.searchid === serverPokemon.searchid); + + if (clientPokemonIndex > -1) { + return player.pokemon[clientPokemonIndex]; + } + + // at this point, most likely means that the Pokemon is not yet revealed, + // so convert the ServerPokemon into a partially-filled Pokemon object + return > { + ident: serverPokemon.ident, + searchid: serverPokemon.searchid, + name: serverPokemon.name, + speciesForme: serverPokemon.speciesForme, + details: serverPokemon.details, + gender: serverPokemon.gender, + level: serverPokemon.level, + hp: serverPokemon.hp, + maxhp: serverPokemon.maxhp, + }; + }) : player.pokemon; + + // update each pokemon + // (note that the index `i` should be relatively consistent between turns) + for (let i = 0; i < playerPokemon.length; i++) { + const clientPokemon = playerPokemon[i]; + + // l.debug('Processing client Pokemon', clientPokemon.speciesForme, 'for player', playerKey); + + const clientPokemonId = calcPokemonCalcdexId({ + ...clientPokemon, + + // always 0 for some reason, so we'll reuse it for our own purposes ;) + slot: i, + + // may need to specify this for generating a unique calcdexId for the Showdown.Pokemon + side: clientPokemon?.side?.sideid ? clientPokemon.side : { + sideid: playerKey, + }, + }); + + const serverPokemon = isMyPokemonSide ? + // myPokemon.find((p, j) => calcPokemonCalcdexId({ ...p, slot: j }) === clientPokemonId) : + myPokemon.find((p) => p.searchid === clientPokemon.searchid) : + null; + + const matchedPokemonIndex = playerState.pokemon.findIndex((p) => p.calcdexId === clientPokemonId); + const matchedPokemon = matchedPokemonIndex > -1 ? playerState.pokemon[matchedPokemonIndex] : null; + + // this is our starting point for the current clientPokemon + const basePokemon = matchedPokemon || sanitizePokemon({ + ...clientPokemon, + slot: i, // important that we specify this to obtain a consistent calcdexId + }); + + // and then from here on out, we just directly modify syncedPokemon + // (serverPokemon and dex are optional, which will add additional known properties) + const syncedPokemon = syncPokemon( + basePokemon, + clientPokemon, + serverPokemon, + dex, + battleState.format, + ); + + l.debug( + 'Completed initial sync for Pokemon', syncedPokemon.speciesForme, 'of player', playerKey, + '\n', 'syncedPokemon', syncedPokemon, + ); + + // build (or rebuild) the Pokemon's movesets + // (which we can only do here, since it's async) + if (!matchedPokemon || matchedPokemon.speciesForme !== syncedPokemon.speciesForme) { + // l.debug('Fetching learnset for Pokemon', syncedPokemon.speciesForme, 'of player', playerKey); + + const learnset = await dex.learnsets.learnable(syncedPokemon.speciesForme); + + // l.debug( + // 'Fetched learnset for Pokemon', syncedPokemon.speciesForme, 'of player', playerKey, + // '\n', 'learnset', learnset, + // ); + + syncedPokemon.moveState.learnset = Object.keys(learnset || {}) + .map((moveid) => dex.moves.get(moveid)?.name) + .filter((name) => !!name && !syncedPokemon.moveState.revealed.includes(name)) + .sort(); + + // build `other`, only if we have no `learnsets` or the `format` has something to do with hacks + if (!syncedPokemon.moveState.learnset.length || (battleState.format && /anythinggoes|hackmons/i.test(battleState.format))) { + syncedPokemon.moveState.other = Object.keys(BattleMovedex || {}) + .map((moveid) => dex.moves.get(moveid)?.name) + .filter((name) => !!name && !syncedPokemon.moveState.revealed.includes(name) && !syncedPokemon.moveState.learnset?.includes?.(name)) + .sort(); + } + + // l.debug( + // 'Updated moveState for Pokemon', syncedPokemon.speciesForme, 'of player', playerKey, + // '\n', 'syncedPokemon.moveState', syncedPokemon.moveState, + // '\n', 'syncedPokemon', syncedPokemon, + // ); + } + + // add the pokemon to the player's Calcdex state (if not maxed already) + if (!matchedPokemon) { + if (playerState.pokemon.length >= env.int('calcdex-player-max-pokemon')) { + if (__DEV__) { + l.warn( + 'Ignoring adding clientPokemon for', playerKey, 'since they have the max number of Pokemon.', + '\n', 'CALCDEX_PLAYER_MAX_POKEMON', env.int('calcdex-player-max-pokemon'), + '\n', 'slot', i, + '\n', 'clientPokemonId', clientPokemonId, + '\n', 'clientPokemon', clientPokemon, + '\n', 'syncedPokemon', syncedPokemon, + '\n', 'player.pokemon', player.pokemon, + '\n', 'playerState.pokemon', playerState.pokemon, + '\n', '(You will only see this warning on development.)', + ); + } + + continue; + } + + playerState.pokemon.push(syncedPokemon); + + l.debug( + 'Adding new Pokemon', syncedPokemon.speciesForme, 'to player', playerKey, + '\n', 'slot', i, + '\n', 'clientPokemonId', clientPokemonId, + '\n', 'clientPokemon', clientPokemon, + '\n', 'syncedPokemon', syncedPokemon, + '\n', 'playerState.pokemon', playerState.pokemon, + ); + } else { + playerState.pokemon[matchedPokemonIndex] = syncedPokemon; + + l.debug( + 'Updating existing Pokemon', syncedPokemon.speciesForme, 'at index', matchedPokemonIndex, 'for player', playerKey, + '\n', 'clientPokemonId', clientPokemonId, + '\n', 'clientPokemon', clientPokemon, + '\n', 'syncedPokemon', syncedPokemon, + '\n', 'playerState.pokemon', playerState.pokemon, + ); + } + } + + // update activeIndex (and selectionIndex if autoSelect is enabled) + // (hopefully the `searchid` exists here!) + const activeIndex = player.active?.[0]?.searchid ? + // playerPokemon.findIndex((p) => p === player.active[0]) : + playerPokemon.findIndex((p) => p.searchid === player.active[0].searchid) : + -1; + + if (activeIndex > -1) { + playerState.activeIndex = activeIndex; + + if (playerState.autoSelect) { + playerState.selectionIndex = activeIndex; + } + } + } + + const syncedField = syncField( + battleState.field, + battle, + battleState.p1.activeIndex, + battleState.p2.activeIndex, + ); + + if (!syncedField?.gameType) { + if (__DEV__) { + l.warn( + 'Failed to sync the field state from the Showdown battle state.', + '\n', 'syncedField', syncedField, + '\n', 'battleState.field', battleState.field, + '\n', 'battle', battle, + '\n', 'attackerIndex', battleState.p1.activeIndex, 'defenderIndex', battleState.p2.activeIndex, + '\n', 'battleState', battleState, + '\n', '(You will only see this warning on development.)', + ); + } + + return; + } + + battleState.field = syncedField; + + // this is important, otherwise we can't ignore re-renders of the same battle state + // (which may result in reaching React's maximum update depth) + if (battleNonce) { + battleState.battleNonce = battleNonce; + } + + l.debug( + 'DONE', SyncBattleActionType, + '\n', 'battle', battle, + '\n', 'battleState', battleState, + '\n', 'state', state, + ); + + return battleState; + }, +); + +/* eslint-enable no-nested-ternary */ diff --git a/src/redux/services/index.ts b/src/redux/services/index.ts new file mode 100644 index 00000000..985f1b04 --- /dev/null +++ b/src/redux/services/index.ts @@ -0,0 +1,2 @@ +export * from './pkmnApi'; +export * from './presetApi'; diff --git a/src/redux/services/pkmnApi.ts b/src/redux/services/pkmnApi.ts new file mode 100644 index 00000000..8ed232fd --- /dev/null +++ b/src/redux/services/pkmnApi.ts @@ -0,0 +1,33 @@ +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; +import { env } from '@showdex/utils/core'; +import { PokemonReduxTagType } from '@showdex/consts'; + +/** + * Serves as the base query for all calls to the pkmn API. + * + * * Data in this API's reducer may be massive, typically in the thousands. + * - May not be a good idea to enable the Redux `devTools` options since it may cause the + * entire website to hang (and eventually crash). + * + * @warning Do not use this directly. Add `endpoints` by calling `pkmnApi.injectEndpoints()`. + * @since 0.1.3 + */ +export const pkmnApi = createApi({ + reducerPath: 'pkmnApi', + + baseQuery: fetchBaseQuery({ + baseUrl: env('pkmn-presets-base-url'), // e.g., 'https://pkmn.github.io' + + // uses the background service worker to fetch() data from an external source + // (Chrome does not allow an injected extension like this to directly call fetch() due to CORS) + // update: doesn't work -- see the comment in the pokemonGensPreset endpoint for more info + // fetchFn: runtimeFetch, + }), + + tagTypes: [ + PokemonReduxTagType, + ].flatMap((type) => Object.values(type)), + + // do not add endpoints here; inject them in other files for code-splitting + endpoints: () => ({}), +}); diff --git a/src/redux/services/presetApi.ts b/src/redux/services/presetApi.ts new file mode 100644 index 00000000..e23125a5 --- /dev/null +++ b/src/redux/services/presetApi.ts @@ -0,0 +1,231 @@ +import { HttpMethod, PokemonReduxTagType } from '@showdex/consts'; +import { env, runtimeFetch } from '@showdex/utils/core'; +import { + createTagProvider, + transformPresetResponse, + transformRandomsPresetResponse, +} from '@showdex/utils/redux'; +import type { + AbilityName, + GenerationNum, + ItemName, + MoveName, +} from '@pkmn/data'; +import type { CalcdexPokemonPreset } from '@showdex/redux/store'; +import { pkmnApi } from './pkmnApi'; + +/** + * Request arguments for a pkmn API endpoint. + * + * @since 0.1.3 + */ +export interface PkmnSmogonPresetRequest { + gen: GenerationNum; + + /** + * Primarily intended to distinguish BDSP from any other gen. + * + * * BDSP is a special case: + * - For non-randoms, we must pull from Gen 4 since Pokemon like Breloom don't exist in Gen 8, + * despite the format being `'gen8bdsp*'`. + * - For randoms, we must pull from `'gen8bdsprandombattle'`, not `'gen4randombattle'` nor `'gen8randombattle'`. + * + * @example 'gen8bdsprandombattle' + * @since 0.1.3 + */ + format?: string; +} + +/** + * Schema of a (battle) format set for a given Pokemon. + * + * @since 0.1.0 + */ +export interface PkmnSmogonPreset { + ability: AbilityName | AbilityName[]; + nature: Showdown.PokemonNature | Showdown.PokemonNature[]; + item: ItemName | ItemName[]; + moves: (MoveName | MoveName[])[]; + ivs?: Showdown.StatsTable; + evs?: Showdown.StatsTable; +} + +/** + * Downloaded JSON from the Gen Sets API via `@pkmn/smogon`. + * + * * Models the structure of the sets of an entire gen (e.g., `'/gen8.json'`), + * which includes every format in that gen. + * - Incompatible with the structure of the sets of a single format (e.g., `'/gen8ou.json'`), + * which does not have the `format` key wrapping each `PkmnSmogonPreset`. + * * Note that the Randoms API has a different schema, so you should use `PkmnSmogonRandomsPresetResponse` instead. + * * Won't be used as a final type since we'll convert these into `CalcdexPokemonPreset`s + * in the `transformPresetResponse()` function. + * * Updated from v0.1.0, where the original typing was something like: + * `Record>>`. + * - Required lots of manual type assertions, so this is a lot cleaner. + * - No idea why I didn't type it like this in the first place... LOL. + * + * @since 0.1.3 + */ +export interface PkmnSmogonPresetResponse { + [speciesForme: string]: { + [format: string]: { + [presetName: string]: PkmnSmogonPreset; + } + } +} + +/** + * Schema of a randoms set for a given Pokemon. + * + * * Note that in randoms, all Pokemon are given the *Hardy* nature, + * which provides no stat increases/decreases (neutral). + * - There are 4 other neutral natures like *Bashful* and *Serious*, + * but looking at the `@smogon/damage-calc` (aka. ex-`@honko/damage-calc`) code, + * it seems the choice was *Hardy*. + * + * @see https://calc.pokemonshowdown.com/randoms.html + * @since 0.1.0 + */ +export interface PkmnSmogonRandomPreset { + level: number; + abilities: AbilityName[]; + items: ItemName[]; + moves: MoveName[]; + + /** + * Unless specified, all IVs should default to `31`. + * + * @example + * ```ts + * // results in IVs: 31 HP, 0 ATK, 31 DEF, 31 SPA, 31 SPD, 31 SPE + * { atk: 0 } + * ``` + * @since 0.1.0 + */ + ivs?: Showdown.StatsTable; + + /** + * Unless specified, all EVs should default to `84`. + * + * * Why 84? Since you can only have total of 508 EVs, considering there are 6 different stats, + * we can apply a simple mathematical algorithm to arrive at the value 84 for each stat. + * - Technically, 508 ÷ 6 is 84.6667, but we floor the value to 84. + * - Why 508? Because Pokemon said so. ¯\_(ツ)_/¯ + * - Also for non-Chinese EVs, you typically apply 252 EVs to 2 stats and the remaining 4 EVs + * to another, so 252 + 252 + 4 = 508. + * - Showdown's Teambuilder also reports a max of 508 EVs. + * + * @example + * ```ts + * // results in EVs: 84 HP, 84 ATK, 84 DEF, 84 SPA, 84 SPD, 0 SPE + * // (yes, this doesn't add up to 508 EVs, but that's how random sets work apparently) + * { spe: 0 } + * ``` + * @see https://calc.pokemonshowdown.com/randoms.html + * @since 0.1.0 + */ + evs?: Showdown.StatsTable; +} + +/** + * Downloaded JSON from the Randoms API via `@pkmn/smogon`. + * + * * Note that the schema is different from that of the Gen Sets API, + * as outlined in the `PkmnSmogonPresetResponse` interface. + * * Won't be used as a final type since we'll convert these into `CalcdexPokemonPreset`s + * in the `transformRandomsPresetResponse()` function. + * - Also note the slight difference in function's name, as it includes "Randoms". + * - Function without "Randoms" is for transforming the response from the Gen Sets API. + * + * @since 0.1.0 + */ +export interface PkmnSmogonRandomsPresetResponse { + [speciesForme: string]: PkmnSmogonRandomPreset; +} + +export const presetApi = pkmnApi.injectEndpoints({ + overrideExisting: true, + + endpoints: (build) => ({ + pokemonGensPreset: build.query({ + // using the fetchBaseQuery() with runtimeFetch() as the fetchFn doesn't seem to work + // (Chrome reports a TypeError when calling fetch() in the background service worker) + // query: ({ gen, format }) => ({ + // url: [ + // env('pkmn-presets-gens-path'), // e.g., '/smogon/data/sets' + // `gen${format?.includes('bdsp') ? 4 : gen}.json`, // e.g., 'gen8.json' + // ].join('/'), // e.g., '/smogon/data/sets/gen8.json' + // method: HttpMethod.GET, + // }), + + // since this is the workaround, we must manually fetch the data and transform the response + // (not a big deal though... considering the hours I've spent pulling my hair out LOL) + queryFn: async ({ gen, format }) => { + const response = await runtimeFetch([ + env('pkmn-presets-base-url'), + env('pkmn-presets-gens-path'), // e.g., '/smogon/data/sets' + `gen${format?.includes('bdsp') ? 4 : gen}.json`, // e.g., 'gen8.json' + ].join('/'), { + method: HttpMethod.GET, + headers: { + Accept: 'application/json', + }, + }); + + const data = response.json(); + + return { + data: transformPresetResponse(data, null, { + gen, + format, + }), + }; + }, + + // transformResponse: transformPresetResponse, + providesTags: createTagProvider(PokemonReduxTagType.Preset), + }), + + pokemonRandomsPreset: build.query({ + // (see the comment in the pokemonGensPreset endpoint as to why we're using queryFn here) + // query: ({ gen, format }) => ({ + // url: [ + // env('pkmn-presets-randoms-path'), // e.g., '/randbats/data' + // `gen${format?.includes('bdsp') ? '8bdsp' : gen}randombattle.json`, // e.g., 'gen8randombattle.json' + // ].join('/'), // e.g., '/randbats/data/gen8randombattle.json' + // method: HttpMethod.GET, + // }), + + queryFn: async ({ gen, format }) => { + const response = await runtimeFetch([ + env('pkmn-presets-base-url'), + env('pkmn-presets-randoms-path'), // e.g., '/randbats/data' + `gen${format?.includes('bdsp') ? '8bdsp' : gen}randombattle.json`, // e.g., 'gen8randombattle.json' + ].join('/'), { + method: HttpMethod.GET, + headers: { + Accept: 'application/json', + }, + }); + + const data = response.json(); + + return { + data: transformRandomsPresetResponse(data, null, { + gen, + format, + }), + }; + }, + + // transformResponse: transformRandomsPresetResponse, + providesTags: createTagProvider(PokemonReduxTagType.Preset), + }), + }), +}); + +export const { + usePokemonGensPresetQuery, + usePokemonRandomsPresetQuery, +} = presetApi; diff --git a/src/redux/store/calcdexSlice.ts b/src/redux/store/calcdexSlice.ts new file mode 100644 index 00000000..9dfd7dbb --- /dev/null +++ b/src/redux/store/calcdexSlice.ts @@ -0,0 +1,938 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { syncBattle } from '@showdex/redux/actions'; +import { sanitizeField } from '@showdex/utils/battle'; +import { calcPokemonCalcdexId } from '@showdex/utils/calc'; +import { env } from '@showdex/utils/core'; +import { logger } from '@showdex/utils/debug'; +import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; +import type { + AbilityName, + GenerationNum, + ItemName, + MoveName, +} from '@pkmn/data'; +import type { State as SmogonState } from '@smogon/calc'; +import { useSelector } from './hooks'; + +export interface CalcdexMoveState { + /** + * Should only consist of moves that were revealed during the battle. + * + * * These moves should have the highest render priority + * (i.e., should be at the top of the list). + * * This is usually accessible within the client `Showdown.Pokemon` object, + * under the `moveTrack` property. + * + * @default + * ```ts + * [] + * ``` + * @since 0.1.0 + */ + revealed: (MoveName | string)[]; + + /** + * Should only consist of moves that the Pokemon can legally learn. + * + * * These moves should be rendered after those in `revealed`. + * * Moves that exist in `revealed` should be filtered out. + * + * @default + * ```ts + * [] + * ``` + * @since 0.1.0 + */ + learnset: (MoveName | string)[]; + + /** + * Optional moves, including potentially illegal ones for formats like `gen8anythinggoes` (I think lmao). + * + * * These moves, if specified, should be rendered last. + * * Moves that exist in `revealed` and `learnsets` should be filtered out. + * + * @default + * ```ts + * [] + * ``` + * @since 0.1.0 + */ + other: (MoveName | string)[]; +} + +/* eslint-disable @typescript-eslint/indent */ + +/** + * Pokemon set, ~~basically~~ probably. + * + * * Types for some properties are more specifically typed, + * such as defining `item` as type `ItemName` instead of `string` (from generic `T` in `PokemonSet`). + * + * Any mention of the word *preset* here (and anywhere else within the code) is meant to be used interchangably with *set*. + * * Avoids potential confusion (and hurting yourself in that confusion) by avoiding the JavaScript keyword `set`. + * * Similar to naming a property *delete*, you can imagine having to destructure `delete` as a variable! + * * Also, `setSet()` or `setPreset()`? Hmm... + * + * @since 0.1.0 + */ +export interface CalcdexPokemonPreset { + /** + * Unique ID (via `uuid`) generated from a serialized checksum of this preset. + * + * * For more information about why this property exists, + * see the `name` property. + * * Note that a preset won't have a `calcdexNonce` property since none of the preset's + * properties should be mutable (they're pre-*set*, after all!). + * + * @since 0.1.0 + */ + calcdexId?: string; + + /** + * Alias of `calcdexId`, used internally by RTK Query in its internal tagging system. + * + * * Wherever the `calcdexId` is set, this property will be set to the same value as well. + * * Recommended you use `calcdexId` over this property to avoid confusion. + * + * @since 0.1.3 + */ + id?: string; + + /** + * Name of the preset. + * + * * Unfortunately, when accessing the presets via `smogon.sets()` in `@pkmn/smogon` without a `format` argument, + * none of the presets have their tier prefixed to the name. + * - e.g., "OU Choice Band" (how Smogon does it) vs. "Choice Band" (what `smogon.sets()` returns) + * * This makes it difficult to differentiate presets between each tier, + * especially if you're using the `name` as the value. + * - e.g., "OU Choice Band" and "UU Choice Band" will both have the `name` "Choice Band". + * * For indexing, it's better to use the `calcdexId` property, + * which is calculated from the actual preset values themselves. + * - For instance, "OU Choice Band" may run a different item/nature/moveset than "UU Choice Band", + * resulting in a different `calcdexId`. + * + * @example 'Choice Band' + * @since 0.1.0 + */ + name?: string; + + gen?: GenerationNum; + format?: string; + speciesForme?: string; + level?: number; + gender?: Showdown.GenderName; + shiny?: boolean; + ability?: AbilityName; + altAbilities?: AbilityName[]; + item?: ItemName; + altItems?: ItemName[]; + moves?: MoveName[]; + altMoves?: MoveName[]; + nature?: Showdown.PokemonNature; + ivs?: Showdown.StatsTable; + evs?: Showdown.StatsTable; + happiness?: number; + pokeball?: string; + hpType?: string; + gigantamax?: boolean; +} + +/** + * Lean version of the `Showdown.Pokemon` object used by the official client. + * + * * Basically `Showdown.Pokemon` without the class functions like `isGrounded()`. + * + * @since 0.1.0 + */ +export type CalcdexLeanPokemon = Omit>, + | 'ability' + | 'baseAbility' + | 'item' + | 'hpcolor' + | 'moves' + | 'moveTrack' + | 'nature' + | 'prevItem' + | 'side' + | 'sprite' +>; + +/* eslint-enable @typescript-eslint/indent */ + +export interface CalcdexPokemon extends CalcdexLeanPokemon { + /** + * Internal unqiue ID used by the extension. + * + * @since 0.1.0 + */ + calcdexId?: string; + + /** + * Internal checksum of the Pokemon's mutable properties used by the extension. + * + * @deprecated As of v0.1.3, although assigned, don't think this is used anymore. + * @since 0.1.0 + */ + calcdexNonce?: string; + + /** + * Whether the Pokemon object originates from the client or server. + * + * * If the type if `Showdown.Pokemon`, then the Pokemon is *probably* from the client. + * * If the type is `Showdown.ServerPokemon`, then the Pokemon is from the server (duh). + * * ~~Used to determine which fields to overwrite when syncing.~~ + * - (See deprecation notice below.) + * + * @default false + * @deprecated As of v0.1.3, although assigned, don't think this is used anymore. + * @since 0.1.0 + */ + serverSourced?: boolean; + + /** + * Unsanitized version of `speciesForme`, primarily used for determining Z/Max/G-Max moves. + * + * @since 0.1.2 + */ + rawSpeciesForme?: string; + + /** + * Current types of the Pokemon. + * + * * Could change depending on the Pokemon's ability, like *Protean*. + * * Should be set via `tooltips.getPokemonTypes()`. + * + * @since 0.1.0 + */ + types?: Showdown.TypeName[]; + + /** + * Ability of the Pokemon. + * + * @since 0.1.0 + */ + ability?: AbilityName; + + /** + * Ability of the Pokemon, but it's filthy af. + * + * * Stank. + * + * @since 0.1.0 + */ + dirtyAbility?: AbilityName; + + /** + * Base ability of the Pokemon. + * + * @since 0.1.0 + */ + baseAbility?: AbilityName; + + /** + * Some abilities are conditionally toggled, such as *Flash Fire*. + * + * * While we don't have to worry about those conditions, + * we need to keep track of whether the ability is active. + * * If the ability is not in `PokemonToggleAbilities` in `consts`, + * this value will always be `true`, despite the default value being `false`. + * + * @see `PokemonToggleAbilities` in `src/consts/abilities.ts`. + * @default false + * @since 0.1.2 + */ + abilityToggled?: boolean; + + /** + * Possible abilities of the Pokemon. + * + * @default + * ```ts + * [] + * ``` + * @since 0.1.0 + */ + abilities?: AbilityName[]; + + /** + * Alternative abilities from the currently applied `preset`. + * + * @since 0.1.0 + */ + altAbilities?: AbilityName[]; + + /** + * Nature of the Pokemon. + * + * @since 0.1.0 + */ + nature?: Showdown.PokemonNature; + + /** + * Possible natures of the Pokemon. + * + * @deprecated Use `PokemonNatures` from `@showdex/consts` instead. + * @default + * ```ts + * [] + * ``` + * @since 0.1.0 + */ + natures?: Showdown.PokemonNature[]; + + /** + * Item being held by the Pokemon. + * + * * Unlike `dirtyItem`, any falsy value (i.e., `''`, `null`, or `undefined`) is considered to be *no item*. + * * This (and `prevItem`) is redefined with the `ItemName` type to make `@pkmn/*` happy. + * + * @since 0.1.0 + */ + item?: ItemName; + + /** + * Alternative items from the currently applied `preset`. + * + * @since 0.1.0 + */ + altItems?: ItemName[]; + + /** + * Keeps track of the user-modified item as to not modify the actual `item` (or lack thereof) synced from the `battle` state. + * + * * Since it's possible for a Pokemon to have no item, an empty string (i.e., `''`) will indicate that the Pokemon intentionally has no item. + * - You will need to cast the empty string to type `ItemName` (e.g., ` ''`) since it doesn't exist on that type. + * - Currently not unioning an empty string with `ItemName` since TypeScript will freak the fucc out. + * * Any other falsy value (i.e., `null` or `undefined`) will fallback to the Pokemon's `item` (or lack thereof, if it got *Knocked Off*, for instance). + * - Under-the-hood, the *Nullish Coalescing Operator* (i.e., `??`) is being used, which falls back to the right-hand value if the left-hand value is `null` or `undefined`. + * + * @default null + * @since 0.1.0 + */ + dirtyItem?: ItemName; + + /** + * Previous item that was held by the Pokemon. + * + * * Typically used to keep track of knocked-off or consumed items. + * + * @since 0.1.0 + */ + prevItem?: ItemName; + + /** + * Individual Values (IVs) of the Pokemon. + * + * @since 0.1.0 + */ + ivs?: Showdown.PokemonSet['ivs']; + + /** + * Effort Values (EVs) of the Pokemon. + * + * @since 0.1.0 + */ + evs?: Showdown.PokemonSet['evs']; + + /** + * Moves currently assigned to the Pokemon. + * + * * Typically contains moves set via user input or Smogon sets. + * * Should not be synced with the current `app.curRoom.battle` state. + * - Unless the originating Pokemon object is a `Showdown.ServerPokemon`. + * - In that instance, `serverSourced` should be `true`. + * + * @since 0.1.0 + */ + moves?: MoveName[]; + + /** + * Alternative moves from the currently applied `preset`. + * + * * Should be rendered within the moves dropdown, similar to the moves in the properties of `moveState`. + * * For instance, there may be more than 4 moves from a random preset. + * - The first 4 moves are set to `moves`. + * - All possible moves from the preset (including the 4 that were set to `moves`) are set to this property. + * + * @since 0.1.0 + */ + altMoves?: MoveName[]; + + /** + * Whether the Pokemon is using Z/Max/G-Max moves. + * + * * Using the term *ultimate* (thanks Blizzard/Riot lmaoo) to cover the nomenclature for both Z (gen 7) and Max/G-Max (gen 8) moves. + * + * @since 0.1.2 + */ + useUltimateMoves?: boolean; + + /** + * Moves revealed by the Pokemon to the opponent. + * + * @since 0.1.0 + */ + moveTrack?: [moveName: MoveName, ppUsed: number][]; + + /** + * Categorized moves of the Pokemon. + * + * @since 0.1.0 + */ + moveState?: CalcdexMoveState; + + /** + * Keeps track of user-modified boosts as to not modify the actual boosts from the `battle` state. + * + * @default + * ```ts + * {} + * ``` + * @since 0.1.0 + */ + dirtyBoosts?: Partial>; + + /** + * Base stats of the Pokemon based on its species. + * + * @default + * ```ts + * { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0 } + * ``` + * @since 0.1.0 + */ + baseStats?: Partial; + + /** + * Calculated stats of the Pokemon based on its current properties. + * + * @default + * ```ts + * { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0 } + * ``` + * @since 0.1.0 + */ + calculatedStats?: Partial; + + /** + * Whether to calculate move damages as critical hits. + * + * @default false + * @since 0.1.0 + */ + criticalHit?: boolean; + + /** + * Remaining number of turns the Pokemon is poisoned for. + * + * * This property is only used by `calculate()` in `@smogon/calc`. + * * Value of `0` means the Pokemon is not poisoned. + * + * @default 0 + * @since 0.1.0 + */ + toxicCounter?: number; + + /** + * Preset that's currently being applied to the Pokemon. + * + * * Could use the preset's `name`, but you may run into some issues with uniqueness. + * - See the `name` property in `CalcdexPokemonPreset` for more information. + * * Recommended you use the preset's `calcdexId` as this property's value instead. + * + * @since 0.1.0 + */ + preset?: string; + + /** + * Available presets (i.e., sets) for the Pokemon. + * + * @todo change this to `string[]` (of calcdexId's) for better memory management + * @default + * ```ts + * [] + * ``` + * @since 0.1.0 + */ + presets?: CalcdexPokemonPreset[]; + + /** + * Whether the preset should automatically update based on revealed moves (i.e., `moveState.revealed`). + * + * @default true + * @since 0.1.0 + */ + autoPreset?: boolean; +} + +/* eslint-disable @typescript-eslint/indent */ + +/** + * Lean version of the `Showdown.Side` object used by the official client. + * + * * Basically `Showdown.Side` without the class functions like `addSideCondition()`. + * + * @since 0.1.0 + */ +export type CalcdexLeanSide = Partial, + | 'active' + | 'ally' + | 'battle' + | 'foe' + | 'lastPokemon' + | 'missedPokemon' + | 'pokemon' + | 'wisher' + | 'x' + | 'y' + | 'z' +>>; + +/* eslint-enable @typescript-eslint/indent */ + +export interface CalcdexPlayer extends CalcdexLeanSide { + /** + * Nonce of the player, but not sure if this is actually being used anymore. + * + * @deprecated Probably not being used anymore. + * @since 0.1.0 + */ + calcdexNonce?: string; + + /** + * Index of the `CalcdexPokemon` that is currently active on the field. + * + * @default -1 + * @since 0.1.0 + */ + activeIndex?: number; + + /** + * Index of the `CalcdexPokemon` that the user is currently viewing. + * + * @default 0 + * @since 0.1.0 + */ + selectionIndex?: number; + + /** + * Whether `selectionIndex` should automatically update whenever `activeIndex` updates. + * + * @default true + * @since 0.1.2 + */ + autoSelect?: boolean; + + /** + * Keeps track of the ordering of the Pokemon. + * + * * Each element should be some unique identifier for the Pokemon that's hopefully somewhat consistent. + * * Typically should only be used for ordering `myPokemon` on initialization. + * - Array ordering of `myPokemon` switches to place the last-switched in Pokemon first. + * - Since `calcdexId` internally uses the `slot` value, this re-ordering mechanic produces inconsistent IDs. + * - In randoms, assuming `myPokemon` belongs to `'p1'`, `p1.pokemon` will be empty until Pokemon are revealed, + * while `myPokemon` remains populated, but with shifting indices. + * * Not necessary to use this for opponent and spectating players, + * since the ordering of `p1.pokemon` and `p2.pokemon` remains consistent. + * - Even in randoms, the server sends the client each Pokemon as they're revealed, + * and maintains that order in the battle state (again, under `p1.pokemon` and `p2.pokemon`). + * + * @since 0.1.3 + */ + pokemonOrder?: string[]; + + /** + * Player's current Pokemon, all converted into our custom `CalcdexPokemon` objects. + * + * * Does not need to be populated with the maximum number of Pokemon, + * but should not exceed that amount. + * - Maximum can be configured via the `CALCDEX_PLAYER_MAX_POKEMON` environment variable. + * + * @since 0.1.0 + */ + pokemon?: CalcdexPokemon[]; +} + +export type CalcdexPlayerSide = SmogonState.Side; +export type CalcdexBattleField = SmogonState.Field; + +/** + * Key of a given player. + * + * @warning Note that there isn't any support for `'p3'` and `'p4'` players at the moment. + * @since 0.1.0 + */ +export type CalcdexPlayerKey = + | 'p1' + | 'p2' + | 'p3' + | 'p4'; + +export type CalcdexPlayerState = Partial>; + +/** + * Primary state for a given single instance of the Calcdex. + * + * @since 0.1.0 + */ +export interface CalcdexBattleState extends CalcdexPlayerState { + /** + * Derived from `id` of the Showdown `battle` state. + * + * @example 'battle-gen8ubers-1636924535-utpp6tn0eya3q8q05kakyw3k4s97im9pw' + * @since 0.1.0 + */ + battleId: string; + + /** + * Last synced `nonce` of the Showdown `battle` state. + * + * @since 0.1.3 + */ + battleNonce?: string; + + gen: GenerationNum; + format: string; + field: CalcdexBattleField; +} + +/** + * Redux action payload for updating a single `CalcdexBattleState` based on the required `battleId`. + * + * * Specifying a string literal for `TRequired` will also make those properties required, in addition to `battleId`. + * - For example, `CalcdexSliceStateAction<'field'>` will make `field` required. + * + * @since 0.1.3 + */ +export type CalcdexSliceStateAction< + TRequired extends keyof CalcdexBattleState = never, +> = PayloadAction, Required>>>; + +export interface CalcdexSlicePokemonAction { + battleId: string; + playerKey: CalcdexPlayerKey; + pokemon: DeepPartial; +} + +/** + * Key should be the `battleId`, but doesn't have to be. + * + * * For instance, you may have a constant key for initializing a Calcdex to be used in the teambuilder. + * + * @since 0.1.3 + */ +export type CalcdexSliceState = Record; + +const l = logger('@showdex/redux/store/calcdexSlice'); + +export const calcdexSlice = createSlice, string>({ + name: 'calcdex', + + initialState: {}, + + reducers: { + /** + * Initializes an empty Calcdex state. + * + * @since 0.1.3 + */ + init: (state, action: CalcdexSliceStateAction) => { + l.debug( + 'RECV', action.type, + '\n', 'action.payload', action.payload, + ); + + const { battleId } = action.payload; + + if (!battleId) { + l.error('Attempted to initialize a CalcdexBattleState with a falsy battleId.'); + + return; + } + + if (battleId in state) { + if (__DEV__) { + l.warn( + 'CalcdexBattleState for battleId', battleId, 'already exists.', + 'This dispatch will be ignored (no-op).', + '\n', '(You will only see this warning on development.)', + ); + } + + return; + } + + state[battleId] = { + ...action.payload, + + battleId, + battleNonce: null, // make sure we don't set this for the syncBattle() action + gen: action.payload.gen || env.int('calcdex-default-gen'), + format: action.payload.format || null, + field: action.payload.field || sanitizeField(null), + + p1: { + sideid: 'p1', + name: null, + rating: null, + activeIndex: -1, + selectionIndex: 0, + autoSelect: true, + + ...action.payload.p1, + pokemonOrder: [], + pokemon: [], + }, + + p2: { + sideid: 'p2', + name: null, + rating: null, + activeIndex: -1, + selectionIndex: 0, + autoSelect: true, + + ...action.payload.p2, + pokemonOrder: [], + pokemon: [], + }, + }; + + l.debug( + 'DONE', action.type, + '\n', 'action.payload', action.payload, + '\n', `state[${battleId}]`, state[battleId], + ); + }, + + /** + * Updates an existing `CalcdexBattleState`. + * + * @since 0.1.3 + */ + update: (state, action: CalcdexSliceStateAction) => { + l.debug( + 'RECV', action.type, + '\n', 'action.payload', action.payload, + ); + + const { + battleId, + battleNonce, + gen, + format, + } = action.payload; + + if (!battleId) { + l.error('Attempted to initialize a CalcdexBattleState with a falsy battleId.'); + + return; + } + + if (!(battleId in state)) { + l.error('Could not find a CalcdexBattleState with battleId', battleId); + + return; + } + + const currentState = state[battleId]; + + state[battleId] = { + ...currentState, + + battleId: battleId || currentState.battleId, + battleNonce: battleNonce || currentState.battleNonce, + gen: typeof gen === 'number' && gen > 0 ? gen : currentState.gen, + format: format || currentState.format, + }; + + l.debug( + 'DONE', action.type, + '\n', 'action.payload', action.payload, + '\n', 'state.calcdex', state.calcdex, + ); + }, + + /** + * Updates the `field` of a matching `CalcdexBattleState` from the provided `battleId`. + * + * @since 0.1.3 + */ + updateField: (state, action: CalcdexSliceStateAction<'field'>) => { + l.debug( + 'RECV', action.type, + '\n', 'action.payload', action.payload, + ); + + const { + battleId, + field, + } = action.payload; + + if (!battleId) { + l.error('Attempted to initialize a CalcdexBattleState with a falsy battleId.'); + + return; + } + + if (!(battleId in state)) { + l.error('Could not find a CalcdexBattleState with battleId', battleId); + + return; + } + + state[battleId] = { + ...state[battleId], + + field: { + ...state[battleId].field, + ...field, + }, + }; + + l.debug( + 'DONE', action.type, + '\n', 'action.payload', action.payload, + '\n', 'state.calcdex', state.calcdex, + ); + }, + + /** + * Updates a `CalcdexPlayer` of a matching `CalcdexBattleState` from the provided `battleId`. + * + * * You can technically update both players in a single `dispatch()` by providing `p1` and `p2`. + * + * @since 0.1.3 + */ + updatePlayer: (state, action: CalcdexSliceStateAction) => { + l.debug( + 'RECV', action.type, + '\n', 'action.payload', action.payload, + ); + + const { + battleId, + p1, + p2, + } = action.payload; + + if (!battleId) { + l.error('Attempted to initialize a CalcdexBattleState with a falsy battleId.'); + + return; + } + + if (!(battleId in state)) { + l.error('Could not find a CalcdexBattleState with battleId', battleId); + + return; + } + + if (!Object.keys(p1 || {}).length && !Object.keys(p2 || {}).length) { + l.error('Found no player to update!'); + } + + if (p1) { + state[battleId].p1 = { + ...state[battleId].p1, + ...p1, + }; + } + + if (p2) { + state[battleId].p2 = { + ...state[battleId].p2, + ...p2, + }; + } + + l.debug( + 'DONE', action.type, + '\n', 'action.payload', action.payload, + '\n', 'state.calcdex', state.calcdex, + ); + }, + + /** + * Updates a `CalcdexPokemon` of an existing `CalcdexPlayer` of a matching `CalcdexBattleState` from the provided `battleId`. + * + * @since 0.1.3 + */ + updatePokemon: (state, action: PayloadAction) => { + l.debug( + 'RECV', action.type, + '\n', 'action.payload', action.payload, + ); + + const { + battleId, + playerKey, + pokemon, + } = action.payload; + + if (!battleId) { + l.error('Attempted to initialize a CalcdexBattleState with a falsy battleId.'); + + return; + } + + if (!(battleId in state)) { + l.error('Could not find a CalcdexBattleState with battleId', battleId); + + return; + } + + const battleState = state[battleId]; + + if (!(playerKey in battleState)) { + l.error( + 'Could not find player', playerKey, 'in battleId', battleId, + '\n', 'battleState', battleState, + '\n', 'pokemon', pokemon, + ); + + return; + } + + const playerState = battleState[playerKey]; + + const pokemonId = pokemon?.calcdexId || calcPokemonCalcdexId(pokemon); + const pokemonStateIndex = playerState.pokemon.findIndex((p) => p.calcdexId === pokemonId); + const pokemonState = pokemonStateIndex > -1 ? playerState.pokemon[pokemonStateIndex] : null; + + if (!pokemonState) { + l.debug( + 'Could not find Pokemon', pokemonId, 'of player', playerKey, 'in battleId', battleId, + '\n', 'battleState', battleState, + '\n', 'playerState', playerState, + '\n', 'pokemon', pokemon, + ); + } + + playerState.pokemon[pokemonStateIndex] = { + ...pokemonState, + ...pokemon, + }; + + l.debug( + 'DONE', action.type, + '\n', 'action.payload', action.payload, + '\n', 'state', state, + ); + }, + }, + + extraReducers: (build) => void build + .addCase(syncBattle.fulfilled, (state, action) => { + const { battleId } = action.payload; + + if (battleId) { + state[battleId] = action.payload; + } + }), +}); + +export const useCalcdexState = () => useSelector( + (state) => state?.calcdex, +); diff --git a/src/redux/store/createStore.ts b/src/redux/store/createStore.ts new file mode 100644 index 00000000..085abf42 --- /dev/null +++ b/src/redux/store/createStore.ts @@ -0,0 +1,70 @@ +import { configureStore } from '@reduxjs/toolkit'; +// import { setupListeners } from '@reduxjs/toolkit/query/react'; +import { logger } from '@showdex/utils/debug'; +import type { Action, AnyAction, ConfigureStoreOptions } from '@reduxjs/toolkit'; +import { pkmnApi } from '../services'; +import { calcdexSlice } from './calcdexSlice'; + +export type RootStore = ReturnType; +export type RootDispatch = RootStore['dispatch']; + +export interface RootState extends ReturnType { + calcdex: ReturnType; +} + +/** + * Options for the `createStore()` factory. + * + * @since 1.0.0 + */ +export type CreateReduxStoreOptions< + TState = unknown, + TAction extends Action = AnyAction, + // TMiddlewares extends Middlewares = Middlewares, +> = Omit>, 'middleware' | 'reducer'>; + +const l = logger('@showdex/redux/store/createStore'); + +/** + * A friendly abstraction for another friendly abstraction, RTK's `configureStore()`. + * + * @example + * ```tsx + * const store = createStore(); + * const App = ({ Component, pageProps }: AppProps): JSX.Element => ( + * + * + * + * ); + * ``` + * @since 0.1.3 + */ +export const createStore = ( + options?: CreateReduxStoreOptions, +) => { + const store = configureStore({ + ...options, + + // devTools: __DEV__, + devTools: false, // update: see the comments in pkmnApi as to why this is disabled + + reducer: { + [pkmnApi.reducerPath]: pkmnApi.reducer, + [calcdexSlice.name]: calcdexSlice.reducer, + }, + + middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat( + pkmnApi.middleware, + ), + }); + + // required for refetchOnFocus/refetchOnReconnect + // update: don't actually care about this feature here, so we'll just not set it up lmao + // if (typeof store?.dispatch === 'function') { + // setupListeners(store.dispatch); + // } + + l.debug('Created store', store); + + return store; +}; diff --git a/src/redux/store/hooks.ts b/src/redux/store/hooks.ts new file mode 100644 index 00000000..d340fc48 --- /dev/null +++ b/src/redux/store/hooks.ts @@ -0,0 +1,20 @@ +import { + useDispatch as useReduxDispatch, + useSelector as useReduxSelector, +} from 'react-redux'; +import type { TypedUseSelectorHook } from 'react-redux'; +import type { RootState, RootDispatch } from './createStore'; + +/** + * Typed version of `useDispatch()` from `react-redux`. + * + * @since 0.1.0 + */ +export const useDispatch = () => useReduxDispatch(); + +/** + * Typed version of `useSelector()` from `react-redux`. + * + * @since 0.1.0 + */ +export const useSelector: TypedUseSelectorHook = useReduxSelector; diff --git a/src/redux/store/index.ts b/src/redux/store/index.ts new file mode 100644 index 00000000..be8e997b --- /dev/null +++ b/src/redux/store/index.ts @@ -0,0 +1,3 @@ +export * from './calcdexSlice'; +export * from './createStore'; +export * from './hooks'; diff --git a/yarn.lock b/yarn.lock index 8ee68812..15df13e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1096,7 +1096,7 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.9": +"@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.9", "@babel/runtime@^7.9.2": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a" integrity sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw== @@ -1607,6 +1607,16 @@ dependencies: "@react-types/shared" "^3.14.0" +"@reduxjs/toolkit@^1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.8.3.tgz#9c6a9c497bde43a67618d37a4175a00ae12efeb2" + integrity sha512-lU/LDIfORmjBbyDLaqFN2JB9YmAT1BElET9y0ZszwhSBa5Ef3t6o5CrHupw5J1iOXwd+o92QfQZ8OJpwXvsssg== + dependencies: + immer "^9.0.7" + redux "^4.1.2" + redux-thunk "^2.4.1" + reselect "^4.1.5" + "@smogon/calc@^0.6.0": version "0.6.0" resolved "https://registry.yarnpkg.com/@smogon/calc/-/calc-0.6.0.tgz#ffc2bbdb9ee41b09932b0105a81b7032e36db24d" @@ -1728,6 +1738,14 @@ resolved "https://registry.yarnpkg.com/@types/har-format/-/har-format-1.2.8.tgz#e6908b76d4c88be3db642846bb8b455f0bfb1c4e" integrity sha512-OP6L9VuZNdskgNN3zFQQ54ceYD8OLq5IbqO4VK91ORLfOm7WdT/CiT/pHEBSQEqCInJ2y3O6iCm/zGtPElpgJQ== +"@types/hoist-non-react-statics@^3.3.1": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" + integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/html-minifier-terser@^6.0.0": version "6.1.0" resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35" @@ -1879,6 +1897,11 @@ dependencies: "@types/node" "*" +"@types/use-sync-external-store@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" + integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== + "@types/uuid@^8.3.4": version "8.3.4" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" @@ -4178,7 +4201,7 @@ header-case@^2.0.4: capital-case "^1.0.4" tslib "^2.0.3" -hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1: +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -4339,6 +4362,11 @@ image-size@~0.5.0: resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" integrity sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w= +immer@^9.0.7: + version "9.0.15" + resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.15.tgz#0b9169e5b1d22137aba7d43f8a81a495dd1b62dc" + integrity sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ== + immutable@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23" @@ -5698,6 +5726,13 @@ qs@6.10.3: dependencies: side-channel "^1.0.4" +qs@^6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -5759,11 +5794,28 @@ react-is@^16.13.1, react-is@^16.7.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-is@^18.0.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" + integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== + react-lifecycles-compat@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== +react-redux@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.0.2.tgz#bc2a304bb21e79c6808e3e47c50fe1caf62f7aad" + integrity sha512-nBwiscMw3NoP59NFCXFf02f8xdo+vSHT/uZ1ldDwF7XaTpzm+Phk97VT4urYBl5TYAPNVaFm12UHAEyzkpNzRA== + dependencies: + "@babel/runtime" "^7.12.1" + "@types/hoist-non-react-statics" "^3.3.1" + "@types/use-sync-external-store" "^0.0.3" + hoist-non-react-statics "^3.3.2" + react-is "^18.0.0" + use-sync-external-store "^1.0.0" + react-select@^5.4.0: version "5.4.0" resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.4.0.tgz#81f6ac73906126706f104751ee14437bd16798f4" @@ -5830,6 +5882,18 @@ rechoir@^0.7.0: dependencies: resolve "^1.9.0" +redux-thunk@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.1.tgz#0dd8042cf47868f4b29699941de03c9301a75714" + integrity sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q== + +redux@^4.1.2: + version "4.2.0" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13" + integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA== + dependencies: + "@babel/runtime" "^7.9.2" + regenerate-unicode-properties@^10.0.1: version "10.0.1" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz#7f442732aa7934a3740c779bb9b3340dccc1fb56" @@ -5956,6 +6020,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +reselect@^4.1.5: + version "4.1.6" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.6.tgz#19ca2d3d0b35373a74dc1c98692cdaffb6602656" + integrity sha512-ZovIuXqto7elwnxyXbBtCPo9YFEr3uJqj2rRbcOOog1bmu2Ag85M4hixSwFWyaBMKXNgvPaJ9OSu9SkBPIeJHQ== + reserved-words@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/reserved-words/-/reserved-words-0.1.2.tgz#00a0940f98cd501aeaaac316411d9adc52b31ab1" @@ -6850,6 +6919,11 @@ urix@^0.1.0: resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= +use-sync-external-store@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== + user-home@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f" From c9c3081a6c9c50e360a96055e7c72d31b564d665 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Fri, 12 Aug 2022 23:00:20 -0700 Subject: [PATCH 003/143] fix(comps): fixed inputValue not updating with prop changes --- src/components/form/ValueField/ValueField.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/components/form/ValueField/ValueField.tsx b/src/components/form/ValueField/ValueField.tsx index e103c808..e4cc066c 100644 --- a/src/components/form/ValueField/ValueField.tsx +++ b/src/components/form/ValueField/ValueField.tsx @@ -33,6 +33,20 @@ export const ValueField = React.forwardRef(({ // this is only a visual value, so that we don't forcibly change the user's value as they're typing it const [inputValue, setInputValue] = React.useState(input?.value?.toString()); + React.useEffect(() => { + const value = input?.value?.toString(); + + if (active || !value || value === inputValue) { + return; + } + + setInputValue(value); + }, [ + active, + input?.value, + inputValue, + ]); + return (
Date: Fri, 12 Aug 2022 23:01:16 -0700 Subject: [PATCH 004/143] chore: oh ye added redux as a cz scope --- .cz-config.cjs | 1 + 1 file changed, 1 insertion(+) diff --git a/.cz-config.cjs b/.cz-config.cjs index 32c34ba0..1c742ace 100644 --- a/.cz-config.cjs +++ b/.cz-config.cjs @@ -17,6 +17,7 @@ module.exports = { { name: 'assets' }, { name: 'comps' }, { name: 'consts' }, + { name: 'redux' }, { name: 'styles' }, { name: 'types' }, { name: 'utils' }, From 9e40f839bb56619330b3445a758bf689d50a63b3 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Fri, 12 Aug 2022 23:02:03 -0700 Subject: [PATCH 005/143] feat(utils): added env factory and getEnv --- .env | 19 ++++++++-- src/utils/core/createEnvParser.ts | 59 +++++++++++++++++++++++++++++++ src/utils/core/getEnv.ts | 22 ++++++++++++ src/utils/core/index.ts | 1 + 4 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 src/utils/core/createEnvParser.ts create mode 100644 src/utils/core/getEnv.ts diff --git a/.env b/.env index c81cbd5d..134944b2 100644 --- a/.env +++ b/.env @@ -4,7 +4,7 @@ DEV_HOSTNAME="localhost" DEV_PORT=6969 -DEV_HMR_ENABLED=true +# DEV_HMR_ENABLED=true ## ## uuid @@ -12,6 +12,21 @@ DEV_HMR_ENABLED=true UUID_NAMESPACE="b92890ec-ae85-4692-a4b4-e56268f8caa3" +## +## calcdex +## + +CALCDEX_DEFAULT_GEN=8 +CALCDEX_PLAYER_MAX_POKEMON=6 + +## +## pkmn +## + +PKMN_PRESETS_BASE_URL="https://pkmn.github.io" +PKMN_PRESETS_GENS_PATH="/smogon/data/sets" +PKMN_PRESETS_RANDOMS_PATH="/randbats/data" + ## ## showdown ## @@ -22,6 +37,4 @@ SHOWDOWN_USERS_URL="https://pokemonshowdown.com/users" ## smogon ## -SMOGON_PRESETS_URL="https://pkmn.github.io/smogon/data/sets" -SMOGON_RANDOM_PRESETS_URL="https://pkmn.github.io/randbats/data" SMOGON_UNIVERSITY_DEX_URL="https://www.smogon.com/dex" diff --git a/src/utils/core/createEnvParser.ts b/src/utils/core/createEnvParser.ts new file mode 100644 index 00000000..e36fe197 --- /dev/null +++ b/src/utils/core/createEnvParser.ts @@ -0,0 +1,59 @@ +import { constantCase } from 'change-case'; + +export type EnvDict = Record; + +/** + * Convenient factory to parse and read environment variables. + * + * * Accepts any case for the env key, + * such as `SOME_ENV_VARIABLE`, `some-env-variable`, and `SomeEnvVariable`. + * * For Webpack and similar bundlers, you'll need to provide a `dict` since the bundler + * will directly replace any mention of `process.env.NODE_ENV` (for instance) with the actual value. + * - You won't be able to access `process.env` like an object in Node.js environments, + * as `process.env` will be `undefined` during runtime. + * + * @since 0.1.0 + */ +export const createEnvParser = ( + dict: EnvDict = process.env, + debugKey = 'DEBUG', +) => { + const env = ( + key: string, + defaultValue = '', + ): string => dict?.[constantCase(key)] || defaultValue; + + // env type parsers + env.int = ( + key: string, + defaultValue = 0, + ) => parseInt(env(key), 10) || defaultValue; + + env.float = ( + key: string, + defaultValue = 0, + ) => parseFloat(env(key)) || defaultValue; + + env.bool = ( + key: string, + ) => env(key, 'false').toLowerCase() === 'true'; + + // env utilities + env.exists = ( + key: string, + ) => constantCase(key) in dict; + + env.debug = ( + debugSubKey: string, + ) => [ + dict?.NODE_ENV, + process.env.NODE_ENV, + ].includes('development') && env.bool(`${debugKey}_${constantCase(debugSubKey)}`); + + env.cmp = ( + key: string, + testValue: string, + ) => env(key) === testValue; + + return env; +}; diff --git a/src/utils/core/getEnv.ts b/src/utils/core/getEnv.ts new file mode 100644 index 00000000..68f9121d --- /dev/null +++ b/src/utils/core/getEnv.ts @@ -0,0 +1,22 @@ +import type { EnvDict } from './createEnvParser'; +import { createEnvParser } from './createEnvParser'; + +const processEnv: EnvDict = { + NODE_ENV: process.env.NODE_ENV, + + PACKAGE_NAME: process.env.PACKAGE_NAME, + PACKAGE_VERSION: process.env.PACKAGE_VERSION, + PACKAGE_URL: process.env.PACKAGE_URL, + PACKAGE_BUILD_DATE: process.env.PACKAGE_BUILD_DATE, + + CALCDEX_DEFAULT_GEN: process.env.CALCDEX_DEFAULT_GEN, + CALCDEX_PLAYER_MAX_POKEMON: process.env.CALCDEX_PLAYER_MAX_POKEMON, + PKMN_PRESETS_BASE_URL: process.env.PKMN_PRESETS_BASE_URL, + PKMN_PRESETS_GENS_PATH: process.env.PKMN_PRESETS_GENS_PATH, + PKMN_PRESETS_RANDOMS_PATH: process.env.PKMN_PRESETS_RANDOMS_PATH, + SHOWDOWN_USERS_URL: process.env.SHOWDOWN_USERS_URL, + SMOGON_UNIVERSITY_DEX_URL: process.env.SMOGON_UNIVERSITY_DEX_URL, + UUID_NAMESPACE: process.env.UUID_NAMESPACE, +}; + +export const env = createEnvParser(processEnv, 'DEBUG'); diff --git a/src/utils/core/index.ts b/src/utils/core/index.ts index 9bab89f8..bf762788 100644 --- a/src/utils/core/index.ts +++ b/src/utils/core/index.ts @@ -1,3 +1,4 @@ +export * from './getEnv'; export * from './getExtensionId'; export * from './runtimeFetch'; export * from './upsizeArray'; From eb8707a2df2fca1d29415ac799c8426105cb3164 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Fri, 12 Aug 2022 23:02:15 -0700 Subject: [PATCH 006/143] feat(redux): added redux utils --- src/utils/redux/buildQueryUrl.ts | 21 ++ src/utils/redux/createTagInvalidator.ts | 37 +++ src/utils/redux/createTagProvider.ts | 41 ++++ src/utils/redux/index.ts | 4 + src/utils/redux/transformPresetResponse.ts | 248 +++++++++++++++++++++ 5 files changed, 351 insertions(+) create mode 100644 src/utils/redux/buildQueryUrl.ts create mode 100644 src/utils/redux/createTagInvalidator.ts create mode 100644 src/utils/redux/createTagProvider.ts create mode 100644 src/utils/redux/index.ts create mode 100644 src/utils/redux/transformPresetResponse.ts diff --git a/src/utils/redux/buildQueryUrl.ts b/src/utils/redux/buildQueryUrl.ts new file mode 100644 index 00000000..3823e72e --- /dev/null +++ b/src/utils/redux/buildQueryUrl.ts @@ -0,0 +1,21 @@ +import qs from 'qs'; + +export interface ReduxBasePayload { + id: string; +} + +export const buildQueryUrl = ( + url: string, + query?: DeepPartial, +): string => { + if (!Object.keys(query || {}).length) { + return url; + } + + return [ + url, + + // even if the URL length was client-enforced, someone could easily just `curl` a looong URL lol + !!Object.keys(query || {}) && qs.stringify(query), + ].filter(Boolean).join('?'); +}; diff --git a/src/utils/redux/createTagInvalidator.ts b/src/utils/redux/createTagInvalidator.ts new file mode 100644 index 00000000..40dab229 --- /dev/null +++ b/src/utils/redux/createTagInvalidator.ts @@ -0,0 +1,37 @@ +import type { ReduxTagType } from '@showdex/consts'; +import type { ReduxBasePayload } from './buildQueryUrl'; +import type { ReduxProvidedTag } from './createTagProvider'; + +/* eslint-disable @typescript-eslint/indent */ + +export const createTagInvalidator = ( + tagType: ReduxTagType, + additionalTags?: ReduxProvidedTag[] | ((result: TPayload) => ReduxProvidedTag[]), + omitAnyId?: boolean, +) => ( + result: TPayload, +): ReduxProvidedTag[] => { + const tags = typeof additionalTags === 'function' ? + additionalTags(result) : + additionalTags; + + if (!tagType) { + return Array.isArray(tags) ? tags.filter(Boolean) : []; + } + + return [ + ...(Array.isArray(tags) ? tags.filter(Boolean) : []), + + !!result?.id && { + type: tagType, + id: result.id, + }, + + !omitAnyId && { + type: tagType, + id: '*', + }, + ].filter(Boolean); +}; + +/* eslint-enable @typescript-eslint/indent */ diff --git a/src/utils/redux/createTagProvider.ts b/src/utils/redux/createTagProvider.ts new file mode 100644 index 00000000..4f9f1cca --- /dev/null +++ b/src/utils/redux/createTagProvider.ts @@ -0,0 +1,41 @@ +import type { ReduxTagType } from '@showdex/consts'; +import type { ReduxBasePayload } from './buildQueryUrl'; + +export interface ReduxProvidedTag { + type: ReduxTagType; + id: string; +} + +/* eslint-disable @typescript-eslint/indent */ + +export const createTagProvider = ( + tagType: ReduxTagType, + additionalTags?: ReduxProvidedTag[] | ((result: TPayload[]) => ReduxProvidedTag[]), + omitAnyId?: boolean, +) => ( + result: TPayload[], +): ReduxProvidedTag[] => { + const tags = typeof additionalTags === 'function' ? + additionalTags(result) : + additionalTags; + + if (!tagType || !Array.isArray(result)) { + return Array.isArray(tags) ? tags.filter(Boolean) : []; + } + + return [ + ...(Array.isArray(tags) ? tags : []).filter(Boolean), + + ...(result.map((resource) => (resource?.id ? ({ + type: tagType, + id: resource.id, + }) : null)) || []).filter(Boolean), + + !omitAnyId && { + type: tagType, + id: '*', + }, + ].filter(Boolean); +}; + +/* eslint-enable @typescript-eslint/indent */ diff --git a/src/utils/redux/index.ts b/src/utils/redux/index.ts new file mode 100644 index 00000000..a1a6b1af --- /dev/null +++ b/src/utils/redux/index.ts @@ -0,0 +1,4 @@ +export * from './buildQueryUrl'; +export * from './createTagInvalidator'; +export * from './createTagProvider'; +export * from './transformPresetResponse'; diff --git a/src/utils/redux/transformPresetResponse.ts b/src/utils/redux/transformPresetResponse.ts new file mode 100644 index 00000000..9c0a793e --- /dev/null +++ b/src/utils/redux/transformPresetResponse.ts @@ -0,0 +1,248 @@ +// import { FormatLabels } from '@showdex/consts'; +import { calcPresetCalcdexId } from '@showdex/utils/calc'; +import { env } from '@showdex/utils/core'; +// import { logger } from '@showdex/utils/debug'; +import type { GenerationNum } from '@pkmn/types'; +import type { CalcdexPokemonPreset } from '@showdex/redux/store'; +import type { + PkmnSmogonPresetRequest, + PkmnSmogonPresetResponse, + PkmnSmogonRandomsPresetResponse, +} from '@showdex/redux/services'; + +// const l = logger('@showdex/utils/redux/transformPresetResponse'); + +/** + * Transforms the JSON response from the Gen Sets API by converting the object into an array of `CalcdexPokemonPreset`s. + * + * * Meant to be passed directly into the `transformResponse` option of an RTK Query API endpoint. + * - Nothing stopping you from using it directly, though. + * + * @warning Do not use this to transform the response from the Randoms API, due to differing `response` schemas. + * Use `transformRandomsPresetResponse()` instead. + * @since 0.1.3 + */ +export const transformPresetResponse = ( + response: PkmnSmogonPresetResponse, + _meta: unknown, + args: PkmnSmogonPresetRequest, +): CalcdexPokemonPreset[] => { + if (!Object.keys(response || {}).length) { + return []; + } + + // this will be our final return value + const output: CalcdexPokemonPreset[] = []; + + const gen = args?.gen ?? env.int('calcdex-default-gen'); + + // you bet your ass this is O(n^3), but not only that, + // we're getting a bunch of formats and sets back from the API, all nested objects. + // what's efficiency?? + Object.entries(response).forEach(([ + speciesForme, + formats, + ]) => { + if (!speciesForme || !Object.keys(formats || {}).length) { + return; + } + + Object.entries(formats).forEach(([ + format, + presets, + ]) => { + if (!format || !Object.keys(presets || {}).length) { + return; + } + + // const formatLabel = format in FormatLabels ? + // FormatLabels[format] : + // format?.toUpperCase?.().slice(0, 3); // truncate to 3 chars + + Object.entries(presets).forEach(([ + presetName, + pkmnPreset, + ]) => { + if (!presetName || !Object.keys(pkmnPreset || {}).length) { + return; + } + + const { + ability, + nature, + item, + moves, + ivs, + evs, + } = pkmnPreset; + + const flatMoves = moves?.flatMap((move) => move) ?? []; + + const preset: CalcdexPokemonPreset = { + calcdexId: null, // we'll hash this after we build the object + id: null, // will equal calcdexId, so the same applies as above + + name: presetName, // e.g., 'Defensive Pivot' + gen, + format, // 'ou' + speciesForme, // do not sanitize + + ability: Array.isArray(ability) ? ability[0] : ability, + altAbilities: Array.isArray(ability) ? ability : [ability].filter(Boolean), + + nature: Array.isArray(nature) ? nature[0] : nature, + + item: Array.isArray(item) ? item[0] : item, + altItems: Array.isArray(item) ? item : [item].filter(Boolean), + + moves: moves?.map((move) => (Array.isArray(move) ? move[0] : move)) ?? [], + altMoves: flatMoves.filter((m, i) => !flatMoves.includes(m, i + 1)), // remove dupe moves + + ivs: { + hp: typeof ivs?.hp === 'number' ? ivs.hp : 31, + atk: typeof ivs?.atk === 'number' ? ivs.atk : 31, + def: typeof ivs?.def === 'number' ? ivs.def : 31, + spa: typeof ivs?.spa === 'number' ? ivs.spa : 31, + spd: typeof ivs?.spd === 'number' ? ivs.spd : 31, + spe: typeof ivs?.spe === 'number' ? ivs.spe : 31, + }, + + evs: { + ...evs, + }, + }; + + preset.calcdexId = calcPresetCalcdexId(preset); + preset.id = preset.calcdexId; // used by RTK Query for tagging + + // shouldn't be the case, but check if the preset already exists in our output + const presetIndex = output.findIndex((p) => p.calcdexId === preset.calcdexId); + + if (presetIndex > -1) { + output[presetIndex] = preset; + } else { + output.push(preset); + } + }); + }); + }); + + // l.debug( + // 'Completed gens preset response transformation from the pkmn API', + // '\n', 'gen', gen, + // '\n', 'response', response, + // '\n', 'output', output, + // ); + + return output; +}; + +/** + * Transforms the JSON response from the Randoms API by converting the object into an array of `CalcdexPokemonPreset`s. + * + * * Meant to be passed directly into the `transformResponse` option of an RTK Query API endpoint. + * - Nothing stopping you from using it directly, though. + * + * @warning Do not use this to transform the response from the Gen Sets API, due to differing `response` schemas. + * Use `transformPresetResponse()` instead. + * @since 0.1.3 + */ +export const transformRandomsPresetResponse = ( + response: PkmnSmogonRandomsPresetResponse, + _meta: unknown, + args: PkmnSmogonPresetRequest, +): CalcdexPokemonPreset[] => { + if (!Object.keys(response || {}).length) { + return []; + } + + // this will be our final return value + const output: CalcdexPokemonPreset[] = []; + + const gen = args?.gen ?? env.int('calcdex-default-gen'); + + // at least this is only O(n) + // ...stonks + Object.entries(response).forEach(([ + speciesForme, + pkmnPreset, + ]) => { + if (!speciesForme || !Object.keys(pkmnPreset || {}).length) { + return; + } + + const { + level, + abilities, + items, + moves, + evs, + ivs, + } = pkmnPreset; + + const preset: CalcdexPokemonPreset = { + calcdexId: null, // we'll hash this after we build the object + id: null, // will equal calcdexId, so the same applies as above + + name: 'Randoms', + gen, + format: args?.format ?? `gen${gen}randombattle`, // e.g., 'gen8randombattle' + speciesForme, // do not sanitize + level, + + ability: abilities?.[0], + altAbilities: abilities, + + // see notes for `PkmnSmogonRandomPreset` in `@showdex/redux/services/pkmnApi` + // for more info about why we Hardy har har here + nature: 'Hardy', + + item: items?.[0], + altItems: items, + + moves: moves?.slice(0, 4), + altMoves: moves, + + ivs: { + hp: ivs?.hp ?? 31, + atk: ivs?.atk ?? 31, + def: ivs?.def ?? 31, + spa: ivs?.spa ?? 31, + spd: ivs?.spd ?? 31, + spe: ivs?.spe ?? 31, + }, + + // see notes for the `evs` property in `PkmnSmogonRandomPreset` in `@showdex/redux/services/pkmnApi` + // for more info about why 84 EVs is the default value for each stat + evs: { + hp: evs?.hp ?? 84, + atk: evs?.atk ?? 84, + def: evs?.def ?? 84, + spa: evs?.spa ?? 84, + spd: evs?.spd ?? 84, + spe: evs?.spe ?? 84, + }, + }; + + preset.calcdexId = calcPresetCalcdexId(preset); + preset.id = preset.calcdexId; // used by RTK Query for tagging + + // shouldn't be the case, but check if the preset already exists in our output + const presetIndex = output.findIndex((p) => p.calcdexId === preset.calcdexId); + + if (presetIndex > -1) { + output[presetIndex] = preset; + } else { + output.push(preset); + } + }); + + // l.debug( + // 'Completed randoms preset response transformation from the pkmn API', + // '\n', 'gen', gen, + // '\n', 'response', response, + // '\n', 'output', output, + // ); + + return output; +}; From b53b7398ebdcd8441cccc557248221a480a991df Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Fri, 12 Aug 2022 23:02:42 -0700 Subject: [PATCH 007/143] refac(utils): replaced process.env w/ getEnv in printBuildInfo --- src/utils/debug/printBuildInfo.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/utils/debug/printBuildInfo.ts b/src/utils/debug/printBuildInfo.ts index aef073bc..519fd532 100644 --- a/src/utils/debug/printBuildInfo.ts +++ b/src/utils/debug/printBuildInfo.ts @@ -1,6 +1,8 @@ +import { env } from '@showdex/utils/core'; + export const printBuildInfo = (): string => ( - process.env.PACKAGE_NAME + - `-v${process.env.PACKAGE_VERSION}` + - `-b${process.env.PACKAGE_BUILD_DATE}` + + env('package-name') + + `-v${env('package-version')}` + + `-b${env('package-build-date')}` + `${__DEV__ ? '-dev' : ''}` ); From ae41247d514185588993af7d0943254490169447 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Fri, 12 Aug 2022 23:03:26 -0700 Subject: [PATCH 008/143] feat(comps): moved getColorScheme to app utils --- src/components/app/ColorScheme/ColorSchemeProvider.tsx | 2 +- .../app/ColorScheme => utils/app}/getColorScheme.ts | 2 +- src/utils/app/index.ts | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) rename src/{components/app/ColorScheme => utils/app}/getColorScheme.ts (95%) diff --git a/src/components/app/ColorScheme/ColorSchemeProvider.tsx b/src/components/app/ColorScheme/ColorSchemeProvider.tsx index 174d6400..4817b572 100644 --- a/src/components/app/ColorScheme/ColorSchemeProvider.tsx +++ b/src/components/app/ColorScheme/ColorSchemeProvider.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; +import { getColorScheme } from '@showdex/utils/app'; import { ColorSchemeContext } from './ColorSchemeContext'; -import { getColorScheme } from './getColorScheme'; export interface ColorSchemeProviderProps { children?: React.ReactNode; diff --git a/src/components/app/ColorScheme/getColorScheme.ts b/src/utils/app/getColorScheme.ts similarity index 95% rename from src/components/app/ColorScheme/getColorScheme.ts rename to src/utils/app/getColorScheme.ts index 5aca31b9..e3afe689 100644 --- a/src/components/app/ColorScheme/getColorScheme.ts +++ b/src/utils/app/getColorScheme.ts @@ -1,4 +1,4 @@ -type ColorScheme = +export type ColorScheme = | 'light' | 'dark'; diff --git a/src/utils/app/index.ts b/src/utils/app/index.ts index 8ebfde30..a4f6cb5b 100644 --- a/src/utils/app/index.ts +++ b/src/utils/app/index.ts @@ -1,7 +1,6 @@ export * from './createSideRoom'; export * from './getActiveBattle'; export * from './getBattleRoom'; -export * from './getMaxMove'; -export * from './getZMove'; +export * from './getColorScheme'; export * from './openShowdownUser'; export * from './openSmogonUniversity'; From 7bae1289a736e7f0ff6128d5ba4c7a043f777504 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Fri, 12 Aug 2022 23:04:15 -0700 Subject: [PATCH 009/143] feat(consts): added httpMethod and redux tagTypes --- src/consts/httpMethod.ts | 27 +++++++++++++++++++++++++++ src/consts/index.ts | 2 ++ src/consts/tagTypes.ts | 11 +++++++++++ 3 files changed, 40 insertions(+) create mode 100644 src/consts/httpMethod.ts create mode 100644 src/consts/tagTypes.ts diff --git a/src/consts/httpMethod.ts b/src/consts/httpMethod.ts new file mode 100644 index 00000000..a26a52d1 --- /dev/null +++ b/src/consts/httpMethod.ts @@ -0,0 +1,27 @@ +export enum HttpMethod { + ALL = 'all', + CHECKOUT = 'checkout', + COPY = 'copy', + DELETE = 'delete', + GET = 'get', + HEAD = 'head', + LOCK = 'lock', + MERGE = 'merge', + MKACTIVITY = 'mkactivity', + MKCOL = 'mkcol', + MOVE = 'move', + MSEARCH = 'm-search', + NOTIFY = 'notify', + OPTIONS = 'options', + PATCH = 'patch', + POST = 'post', + PURGE = 'purge', + PUT = 'put', + REPORT = 'report', + SEARCH = 'search', + SUBSCRIBE = 'subscribe', + TRACE = 'trace', + UNLOCK = 'unlock', + UNSUBSCRIBE = 'unsubscribe', + USE = 'use', +} diff --git a/src/consts/index.ts b/src/consts/index.ts index 542b6f59..f4e510c7 100644 --- a/src/consts/index.ts +++ b/src/consts/index.ts @@ -1,6 +1,8 @@ export * from './abilities'; export * from './ansiColor'; export * from './formats'; +export * from './httpMethod'; export * from './natures'; export * from './stats'; +export * from './tagTypes'; export * from './weather'; diff --git a/src/consts/tagTypes.ts b/src/consts/tagTypes.ts new file mode 100644 index 00000000..91249c33 --- /dev/null +++ b/src/consts/tagTypes.ts @@ -0,0 +1,11 @@ +/** + * For use with RTK Query only. + * + * @since 0.1.3 + */ +export type ReduxTagType = + | PokemonReduxTagType; + +export enum PokemonReduxTagType { + Preset = 'pokemon:preset', +} From e5cc7777d6b19c18ac24851d25be26a9ed99a311 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Fri, 12 Aug 2022 23:05:02 -0700 Subject: [PATCH 010/143] refac(utils): moved getMaxMove and getZMove to battle utils --- src/utils/{app => battle}/getMaxMove.ts | 0 src/utils/{app => battle}/getZMove.ts | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/utils/{app => battle}/getMaxMove.ts (100%) rename src/utils/{app => battle}/getZMove.ts (100%) diff --git a/src/utils/app/getMaxMove.ts b/src/utils/battle/getMaxMove.ts similarity index 100% rename from src/utils/app/getMaxMove.ts rename to src/utils/battle/getMaxMove.ts diff --git a/src/utils/app/getZMove.ts b/src/utils/battle/getZMove.ts similarity index 100% rename from src/utils/app/getZMove.ts rename to src/utils/battle/getZMove.ts From ef5c7daeb2752aabdd854b0ba84ddae1ce36d65d Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Fri, 12 Aug 2022 23:07:15 -0700 Subject: [PATCH 011/143] refac(calcdex): cleaned and moved some calcdex utils --- src/pages/Calcdex/sanitizePokemon.ts | 125 ------- src/pages/Calcdex/syncPokemon.ts | 211 ------------ src/utils/battle/detectGenFromFormat.ts | 26 ++ src/utils/battle/detectPlayerKey.ts | 38 ++ src/utils/battle/detectPokemonIdent.ts | 15 + src/utils/battle/detectSpeciesForme.ts | 16 + src/utils/battle/detectStatBoostDelta.ts | 66 ++++ src/utils/battle/detectToggledAbility.ts | 45 +++ src/utils/battle/formatStatBoost.ts | 10 + src/utils/battle/index.ts | 14 + .../Calcdex => utils/battle}/sanitizeField.ts | 2 +- .../battle}/sanitizePlayerSide.ts | 2 +- src/utils/battle/sanitizePokemon.ts | 174 ++++++++++ .../Calcdex => utils/battle}/syncField.ts | 6 +- src/utils/battle/syncPokemon.ts | 324 ++++++++++++++++++ 15 files changed, 733 insertions(+), 341 deletions(-) delete mode 100644 src/pages/Calcdex/sanitizePokemon.ts delete mode 100644 src/pages/Calcdex/syncPokemon.ts create mode 100644 src/utils/battle/detectGenFromFormat.ts create mode 100644 src/utils/battle/detectPlayerKey.ts create mode 100644 src/utils/battle/detectPokemonIdent.ts create mode 100644 src/utils/battle/detectSpeciesForme.ts create mode 100644 src/utils/battle/detectStatBoostDelta.ts create mode 100644 src/utils/battle/detectToggledAbility.ts create mode 100644 src/utils/battle/formatStatBoost.ts create mode 100644 src/utils/battle/index.ts rename src/{pages/Calcdex => utils/battle}/sanitizeField.ts (94%) rename src/{pages/Calcdex => utils/battle}/sanitizePlayerSide.ts (96%) create mode 100644 src/utils/battle/sanitizePokemon.ts rename src/{pages/Calcdex => utils/battle}/syncField.ts (94%) create mode 100644 src/utils/battle/syncPokemon.ts diff --git a/src/pages/Calcdex/sanitizePokemon.ts b/src/pages/Calcdex/sanitizePokemon.ts deleted file mode 100644 index af108d47..00000000 --- a/src/pages/Calcdex/sanitizePokemon.ts +++ /dev/null @@ -1,125 +0,0 @@ -import type { CalcdexPokemon } from './CalcdexReducer'; -import { calcPokemonCalcdexId } from './calcCalcdexId'; -import { calcPokemonCalcdexNonce } from './calcCalcdexNonce'; -import { detectPokemonIdent } from './detectPokemonIdent'; -import { detectSpeciesForme } from './detectSpeciesForme'; -import { detectToggledAbility } from './detectToggledAbility'; -import { sanitizeSpeciesForme } from './sanitizeSpeciesForme'; - -export const sanitizePokemon = ( - pokemon: Partial, -): CalcdexPokemon => { - const sanitizedPokemon: CalcdexPokemon = { - calcdexId: pokemon?.calcdexId, - calcdexNonce: pokemon?.calcdexNonce, - - ident: detectPokemonIdent(pokemon), - searchid: pokemon?.searchid, - speciesForme: pokemon?.speciesForme ? - sanitizeSpeciesForme(pokemon.speciesForme) : - detectSpeciesForme(pokemon), - rawSpeciesForme: pokemon?.speciesForme, - - name: pokemon?.name, - details: pokemon?.details, - level: pokemon?.level || 0, - gender: pokemon?.gender, - - types: pokemon?.types ?? [], - - ability: pokemon?.ability, - dirtyAbility: pokemon?.dirtyAbility ?? null, - abilityToggled: pokemon?.abilityToggled ?? null, - baseAbility: pokemon?.baseAbility, - abilities: pokemon?.abilities ?? [], - altAbilities: pokemon?.altAbilities ?? [], - - item: pokemon?.item, - dirtyItem: pokemon?.dirtyItem ?? null, - altItems: pokemon?.altItems ?? [], - itemEffect: pokemon?.itemEffect, - prevItem: pokemon?.prevItem, - prevItemEffect: pokemon?.prevItemEffect, - - nature: pokemon?.nature, - - ivs: { - hp: pokemon?.ivs?.hp ?? 31, - atk: pokemon?.ivs?.atk ?? 31, - def: pokemon?.ivs?.def ?? 31, - spa: pokemon?.ivs?.spa ?? 31, - spd: pokemon?.ivs?.spd ?? 31, - spe: pokemon?.ivs?.spe ?? 31, - }, - - evs: { - hp: pokemon?.evs?.hp ?? 0, - atk: pokemon?.evs?.atk ?? 0, - def: pokemon?.evs?.def ?? 0, - spa: pokemon?.evs?.spa ?? 0, - spd: pokemon?.evs?.spd ?? 0, - spe: pokemon?.evs?.spe ?? 0, - }, - - boosts: { - atk: typeof pokemon?.boosts?.atk === 'number' ? pokemon.boosts.atk : 0, - def: typeof pokemon?.boosts?.def === 'number' ? pokemon.boosts.def : 0, - spa: typeof pokemon?.boosts?.spa === 'number' ? pokemon.boosts.spa : 0, - spd: typeof pokemon?.boosts?.spd === 'number' ? pokemon.boosts.spd : 0, - spe: typeof pokemon?.boosts?.spe === 'number' ? pokemon.boosts.spe : 0, - }, - - dirtyBoosts: { - atk: pokemon?.dirtyBoosts?.atk, - def: pokemon?.dirtyBoosts?.def, - spa: pokemon?.dirtyBoosts?.spa, - spd: pokemon?.dirtyBoosts?.spd, - spe: pokemon?.dirtyBoosts?.spe, - }, - - status: pokemon?.status, - statusData: { - sleepTurns: pokemon?.statusData?.sleepTurns || 0, - toxicTurns: pokemon?.statusData?.toxicTurns || 0, - }, - - volatiles: pokemon?.volatiles, - turnstatuses: pokemon?.turnstatuses, - toxicCounter: pokemon?.statusData?.toxicTurns, - - hp: pokemon?.hp || 0, - maxhp: pokemon?.maxhp || 1, - fainted: pokemon?.fainted || !pokemon?.hp, - - moves: pokemon?.moves || [], - altMoves: pokemon?.altMoves || [], - useUltimateMoves: pokemon?.useUltimateMoves ?? false, - lastMove: pokemon?.lastMove, - moveTrack: pokemon?.moveTrack || [], - moveState: { - revealed: pokemon?.moveState?.revealed ?? [], - learnset: pokemon?.moveState?.learnset ?? [], - other: pokemon?.moveState?.other ?? [], - }, - - criticalHit: pokemon?.criticalHit ?? false, - - preset: pokemon?.preset, - presets: pokemon?.presets ?? [], - autoPreset: pokemon?.autoPreset ?? true, - }; - - if (sanitizedPokemon.abilityToggled === null) { - sanitizedPokemon.abilityToggled = detectToggledAbility(pokemon); - } - - const calcdexId = calcPokemonCalcdexId(sanitizedPokemon); - - if (!sanitizedPokemon?.calcdexId) { - sanitizedPokemon.calcdexId = calcdexId; - } - - sanitizedPokemon.calcdexNonce = calcPokemonCalcdexNonce(sanitizedPokemon); - - return sanitizedPokemon; -}; diff --git a/src/pages/Calcdex/syncPokemon.ts b/src/pages/Calcdex/syncPokemon.ts deleted file mode 100644 index 108451c9..00000000 --- a/src/pages/Calcdex/syncPokemon.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { PokemonBoostNames } from '@showdex/consts'; -import { logger } from '@showdex/utils/debug'; -import type { CalcdexMoveState, CalcdexPokemon } from './CalcdexReducer'; -import { calcPokemonCalcdexId } from './calcCalcdexId'; -import { calcPokemonCalcdexNonce } from './calcCalcdexNonce'; -import { detectToggledAbility } from './detectToggledAbility'; -import { sanitizeSpeciesForme } from './sanitizeSpeciesForme'; - -const l = logger('@showdex/pages/Calcdex/syncPokemon'); - -export const syncPokemonBoosts = ( - pokemon: CalcdexPokemon, - mutations: Partial, -): CalcdexPokemon['boosts'] => { - const newPokemon: CalcdexPokemon = { ...pokemon }; - - l.debug( - 'syncPokemonBoosts()', - '\n', 'pokemon', pokemon, - '\n', 'mutations', mutations, - ); - - const boosts = PokemonBoostNames.reduce((prev, stat) => { - const currentValue = prev[stat]; - const value = mutations?.boosts?.[stat] || 0; - - // l.debug( - // 'syncPokemonBoosts()', - // '\n', 'comparing stat', stat, 'currentValue', currentValue, 'with value', value, - // '\n', 'newPokemon', newPokemon?.ident, newPokemon, - // ); - - // l.debug(pokemon.ident, 'comparing value', value, 'and currentValue', currentValue, 'for stat', stat); - - if (value === currentValue) { - return prev; - } - - prev[stat] = value; - - return prev; - }, { - atk: newPokemon?.boosts?.atk || 0, - def: newPokemon?.boosts?.def || 0, - spa: newPokemon?.boosts?.spa || 0, - spd: newPokemon?.boosts?.spd || 0, - spe: newPokemon?.boosts?.spe || 0, - }); - - l.debug( - 'syncPokemonBoosts() -> return boosts', - '\n', 'boosts', boosts, - '\n', 'newPokemon', newPokemon?.ident, newPokemon, - ); - - return boosts; -}; - -export const syncPokemon = ( - pokemon: CalcdexPokemon, - mutations: Partial, -): CalcdexPokemon => { - const newPokemon: CalcdexPokemon = { ...pokemon }; - - ([ - 'name', - 'speciesForme', - 'rawSpeciesForme', - 'hp', - 'maxhp', - 'status', - 'statusData', - 'ability', - 'baseAbility', - 'nature', - 'item', - 'itemEffect', - 'prevItem', - 'prevItemEffect', - 'moveTrack', - 'volatiles', - 'abilityToggled', // should be after volatiles - 'turnstatuses', - 'boosts', - ] as (keyof CalcdexPokemon)[]).forEach((key) => { - const currentValue = newPokemon[key]; // `newPokemon` is the final synced Pokemon that will be returned at the end - let value = mutations?.[key]; // `mutations` is what was changed and may not be a full Pokemon object - - switch (key) { - case 'ability': - case 'nature': { - if (!value) { - return; - } - - break; - } - - case 'item': { - // ignore any unrevealed item (resulting in a falsy value) that hasn't been knocked-off/consumed/etc. - // (this can be checked since when the item be consumed, prevItem would NOT be falsy) - if ((!value || value === '(exists)') && !mutations?.prevItem) { - return; - } - - // clear the dirtyItem if it's what the Pokemon actually has - // (otherwise, if the item hasn't been revealed yet, `value` would be falsy, - // but that's ok cause we have dirtyItem, i.e., no worries about clearing the user's input) - if (value === mutations?.dirtyItem) { - newPokemon.dirtyItem = null; - } - - break; - } - - case 'prevItem': { - // check if the item was knocked-off and is the same as dirtyItem - // if so, clear the dirtyItem - // (note that `value` here is prevItem, NOT item!) - if (mutations?.prevItemEffect === 'knocked off' && value === mutations?.dirtyItem) { - newPokemon.dirtyItem = null; - } - - break; - } - - case 'speciesForme': { - if (mutations?.volatiles?.formechange?.[1]) { - [, value] = mutations.volatiles.formechange; - } - - value = sanitizeSpeciesForme( value); - - break; - } - - case 'rawSpeciesForme': { - value = mutations?.speciesForme ?? newPokemon?.rawSpeciesForme ?? newPokemon?.speciesForme; - - if (!value) { - return; - } - - break; - } - - case 'boosts': { - // value = syncPokemonBoosts(newPokemon, mutations, checkDirty); - value = syncPokemonBoosts(newPokemon, mutations); - - break; - } - - case 'moves': { - if (!( value)?.length) { - return; - } - - break; - } - - case 'moveTrack': { - // l.debug('mutations.moveTrack', mutations?.moveTrack); - - if (mutations?.moveTrack?.length) { - newPokemon.moveState = { - ...newPokemon.moveState, - revealed: mutations.moveTrack.map((track) => track?.[0]), - }; - - // l.debug('value of type CalcdexMoveState set to', newPokemon.moveState); - } - - break; - } - - case 'presets': { - if (!Array.isArray(value) || !value.length) { - value = currentValue; - } - - break; - } - - case 'abilityToggled': { - value = detectToggledAbility(mutations); - - break; - } - - default: break; - } - - if (JSON.stringify(value) === JSON.stringify(currentValue)) { // kekw - return; - } - - /** @see https://github.com/microsoft/TypeScript/issues/31663#issuecomment-518603958 */ - (newPokemon as Record)[key] = value; - }); - - const calcdexId = calcPokemonCalcdexId(newPokemon); - - if (!newPokemon?.calcdexId || newPokemon.calcdexId !== calcdexId) { - newPokemon.calcdexId = calcdexId; - } - - newPokemon.calcdexNonce = calcPokemonCalcdexNonce(newPokemon); - - return newPokemon; -}; diff --git a/src/utils/battle/detectGenFromFormat.ts b/src/utils/battle/detectGenFromFormat.ts new file mode 100644 index 00000000..8bf7e64e --- /dev/null +++ b/src/utils/battle/detectGenFromFormat.ts @@ -0,0 +1,26 @@ +import type { GenerationNum } from '@pkmn/data'; + +const GenRegex = /^gen(\d+)/i; + +/** + * Note that `defaultGen` is `null` by default, but you're free to import `env` + * from `@showdex/utils/core` and set it as `env('calcdex-default-gen')`. + * + * @since 0.1.0 + */ +export const detectGenFromFormat = ( + format: string, + defaultGen: GenerationNum = null, +): GenerationNum => { + if (!GenRegex.test(format)) { + return defaultGen; + } + + const gen = parseInt(format.match(GenRegex)[1], 10); + + if (gen < 1) { + return defaultGen; + } + + return gen; +}; diff --git a/src/utils/battle/detectPlayerKey.ts b/src/utils/battle/detectPlayerKey.ts new file mode 100644 index 00000000..97738b8d --- /dev/null +++ b/src/utils/battle/detectPlayerKey.ts @@ -0,0 +1,38 @@ +import type { CalcdexPlayerKey, CalcdexPokemon } from '@showdex/redux/store'; +import { detectPokemonIdent } from './detectPokemonIdent'; + +export const detectPlayerKeyFromPokemon = ( + pokemon: DeepPartial | DeepPartial | DeepPartial = {}, +): CalcdexPlayerKey => { + const ident = detectPokemonIdent(pokemon); + + if (!ident || !/^p\d+:/.test(ident)) { + return null; + } + + return ident.slice(0, ident.indexOf(':')); +}; + +export const detectPlayerKeyFromBattle = ( + battle: Showdown.Battle, +): CalcdexPlayerKey => { + if (battle?.mySide?.sideid) { + // console.log('battle.mySide.sideid', battle.mySide.sideid, '\n', 'mySide', { + // sideid: battle.mySide.sideid, + // name: battle.mySide.name, + // myPokemon: battle.myPokemon, + // }); + + return battle.mySide.sideid; + } + + if (!battle?.myPokemon?.[0]) { + return null; + } + + const [firstPokemon] = battle.myPokemon; + + // console.log('firstPokemon', firstPokemon); + + return detectPlayerKeyFromPokemon(firstPokemon); +}; diff --git a/src/utils/battle/detectPokemonIdent.ts b/src/utils/battle/detectPokemonIdent.ts new file mode 100644 index 00000000..3219f28f --- /dev/null +++ b/src/utils/battle/detectPokemonIdent.ts @@ -0,0 +1,15 @@ +import type { CalcdexPokemon } from '@showdex/redux/store'; + +export const detectPokemonIdent = ( + pokemon: DeepPartial | DeepPartial | DeepPartial = {}, +): string => [ + ('side' in pokemon ? pokemon.side?.sideid : null) + || pokemon?.ident?.split?.(':')[0], + ('volatiles' in pokemon && pokemon.volatiles?.formechange?.[1]) + || pokemon?.speciesForme + || pokemon?.ident?.split?.(': ')[1] + || pokemon?.details?.split?.(', ')?.[0] + || pokemon?.name, +].filter(Boolean).join(': ') + || pokemon?.ident + || pokemon?.searchid?.split?.('|')[0]; diff --git a/src/utils/battle/detectSpeciesForme.ts b/src/utils/battle/detectSpeciesForme.ts new file mode 100644 index 00000000..934c39ac --- /dev/null +++ b/src/utils/battle/detectSpeciesForme.ts @@ -0,0 +1,16 @@ +import type { CalcdexPokemon } from '@showdex/redux/store'; +import { detectPokemonIdent } from './detectPokemonIdent'; + +/* eslint-disable arrow-body-style */ + +export const detectSpeciesForme = ( + pokemon: DeepPartial | DeepPartial = {}, +): CalcdexPokemon['speciesForme'] => { + // if ('speciesForme' in (pokemon || {})) { + // return sanitizeSpeciesForme(pokemon.speciesForme); + // } + + return pokemon?.volatiles?.formechange?.[1] || + pokemon?.speciesForme || + detectPokemonIdent(pokemon)?.split?.(': ')?.[1]; +}; diff --git a/src/utils/battle/detectStatBoostDelta.ts b/src/utils/battle/detectStatBoostDelta.ts new file mode 100644 index 00000000..075ed880 --- /dev/null +++ b/src/utils/battle/detectStatBoostDelta.ts @@ -0,0 +1,66 @@ +import type { CalcdexPokemon } from '@showdex/redux/store'; + +export type PokemonStatBoostDelta = + | 'positive' + | 'negative'; + +/** + * Determines the direction of the delta of the Pokemon's stat boost. + * + * * In other words, checks whether the Pokemon's current stats are higher or lower than its base stats. + * * In cases where it's neither (i.e., no boosts were applied to the passed-in `stat`), `null` will be returned. + * + * @since 0.1.2 + */ +export const detectStatBoostDelta = ( + pokemon: CalcdexPokemon, + stat: Showdown.StatName, +): PokemonStatBoostDelta => { + if (stat === 'hp') { + return null; + } + + // check for boosts from abilities + if ('slowstart' in (pokemon?.volatiles || {}) && pokemon?.abilityToggled) { + if (['atk', 'spe'].includes(stat)) { + return 'negative'; + } + } + + // check for status-dependent boosts from abilities + const abilitySearchString = pokemon?.ability?.toLowerCase?.(); + const hasGuts = abilitySearchString === 'guts'; + const hasQuickFeet = abilitySearchString === 'quick feet'; + + if (pokemon?.status && pokemon.status !== '???') { + if (hasGuts && stat === 'atk') { + return 'positive'; + } + + if (hasQuickFeet && stat === 'spe') { + return 'positive'; + } + + // may be problematic since we're not using the Pokemon's base stats, + // but oh well, this ok for now lmaoo + if (pokemon.status === 'brn' && stat === 'atk') { + return 'negative'; + } + + if (pokemon.status === 'par' && stat === 'spe') { + return 'negative'; + } + } + + const boost = pokemon?.dirtyBoosts?.[stat] ?? pokemon?.boosts?.[stat] ?? 0; + + if (boost > 0) { + return 'positive'; + } + + if (boost < 0) { + return 'negative'; + } + + return null; +}; diff --git a/src/utils/battle/detectToggledAbility.ts b/src/utils/battle/detectToggledAbility.ts new file mode 100644 index 00000000..3c244212 --- /dev/null +++ b/src/utils/battle/detectToggledAbility.ts @@ -0,0 +1,45 @@ +import { PokemonToggleAbilities } from '@showdex/consts'; +import { calcPokemonHp } from '@showdex/utils/calc'; +import type { AbilityName } from '@pkmn/data'; +import type { CalcdexPokemon } from '@showdex/redux/store'; + +/** + * Determines whether the Pokemon's toggleable ability is active (if applicable). + * + * * Primarily depends on the Pokemon's `volatiles` object (from the `battle` state). + * - For instance, if Heatran's *Flash Fire* ability is activated, you'll see its `volatiles` object + * set to `{ flashfire: ['flashfire'] }` (at the time of writing this, of course). + * - Otherwise, its `ability` would still be `'Flash Fire'`, but `volatiles` would be an empty object, i.e., `{}`. + * * Only exception is the *Multiscale* ability, which is only active when the Pokemon's HP is at 100%, + * similar to how the *Focus Sash* item works. + * - Pretty sure `calculate()` from `@smogon/calc` doesn't care whether `abilityOn` (of `SmogonPokemon`) + * is `true` or `false`, but we keep track of it for visual purposes. + * - (Side note: `SmogonPokemon` mentioned above is an alias of the `Pokemon` class from `@smogon/calc`.) + * - Pokemon's HP value isn't currently editable, so there's no way to toggle *Multiscale* on/off + * (before the Pokemon takes any damage). + * - Additionally, if, say, Dragonite takes damage, this would return `false`, since its HP is no longer at 100%. + * In the case where Dragonite uses *Roost* and the opponent doesn't attack (e.g., switches out), + * resulting in Dragonite healing back to full health (i.e., 100%), this would return `true` again. + * + * @returns `true` if the Pokemon's ability is *toggleable* and *active*, `false` otherwise. + * @since 0.1.2 + */ +export const detectToggledAbility = ( + pokemon: DeepPartial | DeepPartial = {}, +): boolean => { + const ability = pokemon?.ability; + const hasMultiscale = ability?.toLowerCase?.() === 'multiscale'; + const toggleAbility = !hasMultiscale && PokemonToggleAbilities.includes( ability); + + if (hasMultiscale) { + return calcPokemonHp(pokemon) === 1; + } + + if (toggleAbility) { + return Object.keys(pokemon?.volatiles ?? {}).includes( + ability?.replace?.(/\s/g, '').toLowerCase(), + ); + } + + return false; +}; diff --git a/src/utils/battle/formatStatBoost.ts b/src/utils/battle/formatStatBoost.ts new file mode 100644 index 00000000..211928f0 --- /dev/null +++ b/src/utils/battle/formatStatBoost.ts @@ -0,0 +1,10 @@ +export const formatStatBoost = (boostedStat: number): string => { + if (typeof boostedStat !== 'number') { + return ''; + } + + // otherwise, something like '50.400000000000006' can get rendered lol + const isFloat = /\.\d+$/.test(boostedStat.toString()); + + return boostedStat.toFixed(isFloat ? 1 : 0); +}; diff --git a/src/utils/battle/index.ts b/src/utils/battle/index.ts new file mode 100644 index 00000000..0e681deb --- /dev/null +++ b/src/utils/battle/index.ts @@ -0,0 +1,14 @@ +export * from './detectGenFromFormat'; +export * from './detectPlayerKey'; +export * from './detectPokemonIdent'; +export * from './detectSpeciesForme'; +export * from './detectStatBoostDelta'; +export * from './detectToggledAbility'; +export * from './formatStatBoost'; +export * from './getMaxMove'; +export * from './getZMove'; +export * from './sanitizeField'; +export * from './sanitizePlayerSide'; +export * from './sanitizePokemon'; +export * from './syncField'; +export * from './syncPokemon'; diff --git a/src/pages/Calcdex/sanitizeField.ts b/src/utils/battle/sanitizeField.ts similarity index 94% rename from src/pages/Calcdex/sanitizeField.ts rename to src/utils/battle/sanitizeField.ts index 266e24bb..da28ed3f 100644 --- a/src/pages/Calcdex/sanitizeField.ts +++ b/src/utils/battle/sanitizeField.ts @@ -1,5 +1,5 @@ import { PseudoWeatherMap, WeatherMap } from '@showdex/consts'; -import type { CalcdexBattleField } from './CalcdexReducer'; +import type { CalcdexBattleField } from '@showdex/redux/store'; import { sanitizePlayerSide } from './sanitizePlayerSide'; export const sanitizeField = ( diff --git a/src/pages/Calcdex/sanitizePlayerSide.ts b/src/utils/battle/sanitizePlayerSide.ts similarity index 96% rename from src/pages/Calcdex/sanitizePlayerSide.ts rename to src/utils/battle/sanitizePlayerSide.ts index c4008b26..8e69fca8 100644 --- a/src/pages/Calcdex/sanitizePlayerSide.ts +++ b/src/utils/battle/sanitizePlayerSide.ts @@ -1,4 +1,4 @@ -import type { CalcdexPlayerSide } from './CalcdexReducer'; +import type { CalcdexPlayerSide } from '@showdex/redux/store'; export const sanitizePlayerSide = ( player: Showdown.Side, diff --git a/src/utils/battle/sanitizePokemon.ts b/src/utils/battle/sanitizePokemon.ts new file mode 100644 index 00000000..2acaf9b0 --- /dev/null +++ b/src/utils/battle/sanitizePokemon.ts @@ -0,0 +1,174 @@ +import { PokemonNatures } from '@showdex/consts'; +import { calcPokemonCalcdexId, calcPokemonCalcdexNonce } from '@showdex/utils/calc'; +import type { AbilityName, ItemName, MoveName } from '@pkmn/data'; +import type { CalcdexPokemon } from '@showdex/redux/store'; +import { detectPokemonIdent } from './detectPokemonIdent'; +import { detectSpeciesForme } from './detectSpeciesForme'; +import { detectToggledAbility } from './detectToggledAbility'; +// import { sanitizeSpeciesForme } from './sanitizeSpeciesForme'; + +/** + * Essentially converts a `Showdown.Pokemon` into our custom `CalcdexPokemon`. + * + * * Gets in *R E A L / D E E P*. + * - Sanitizes the living shit out of the `pokemon`. + * * You can also pass in an incomplete `CalcdexPokemon`, + * which will fill in defaults for any missing properties. + * - Technically, nothing is required, so you can pass in no arguments and + * still get a partially filled-in `CalcdexPokemon`. + * + * @since 0.1.0 + */ +export const sanitizePokemon = ( + pokemon: DeepPartial | DeepPartial = {}, +): CalcdexPokemon => { + const sanitizedPokemon: CalcdexPokemon = { + calcdexId: ('calcdexId' in pokemon && pokemon.calcdexId) || null, + calcdexNonce: ('calcdexNonce' in pokemon && pokemon.calcdexNonce) || null, + + slot: pokemon?.slot ?? null, // could be 0, so don't use logical OR here + ident: detectPokemonIdent(pokemon), + searchid: pokemon?.searchid, + speciesForme: detectSpeciesForme(pokemon), + rawSpeciesForme: pokemon?.speciesForme, + + name: pokemon?.name, + details: pokemon?.details, + level: pokemon?.level || 0, + gender: pokemon?.gender, + shiny: pokemon?.shiny, + + types: pokemon?.volatiles?.typechange ? + [ pokemon.volatiles.typechange[1]] : + ('types' in pokemon && pokemon.types) || [], + + ability: pokemon?.ability || ('abilities' in pokemon && pokemon.abilities[0]) || null, + dirtyAbility: ('dirtyAbility' in pokemon && pokemon.dirtyAbility) || null, + abilityToggled: 'abilityToggled' in pokemon ? pokemon.abilityToggled : detectToggledAbility(pokemon), + baseAbility: pokemon?.baseAbility, + abilities: ('abilities' in pokemon && pokemon.abilities) || [], + altAbilities: ('altAbilities' in pokemon && pokemon.altAbilities) || [], + + item: pokemon?.item, + dirtyItem: ('dirtyItem' in pokemon && pokemon.dirtyItem) || null, + altItems: ('altItems' in pokemon && pokemon.altItems) || [], + itemEffect: pokemon?.itemEffect, + prevItem: pokemon?.prevItem, + prevItemEffect: pokemon?.prevItemEffect, + + nature: ('nature' in pokemon && pokemon.nature) || PokemonNatures[0], + + ivs: { + hp: ('ivs' in pokemon && pokemon.ivs?.hp) ?? 31, + atk: ('ivs' in pokemon && pokemon.ivs?.atk) ?? 31, + def: ('ivs' in pokemon && pokemon.ivs?.def) ?? 31, + spa: ('ivs' in pokemon && pokemon.ivs?.spa) ?? 31, + spd: ('ivs' in pokemon && pokemon.ivs?.spd) ?? 31, + spe: ('ivs' in pokemon && pokemon.ivs?.spe) ?? 31, + }, + + evs: { + hp: ('evs' in pokemon && pokemon.evs?.hp) ?? 0, + atk: ('evs' in pokemon && pokemon.evs?.atk) ?? 0, + def: ('evs' in pokemon && pokemon.evs?.def) ?? 0, + spa: ('evs' in pokemon && pokemon.evs?.spa) ?? 0, + spd: ('evs' in pokemon && pokemon.evs?.spd) ?? 0, + spe: ('evs' in pokemon && pokemon.evs?.spe) ?? 0, + }, + + boosts: { + atk: typeof pokemon?.boosts?.atk === 'number' ? pokemon.boosts.atk : 0, + def: typeof pokemon?.boosts?.def === 'number' ? pokemon.boosts.def : 0, + spa: typeof pokemon?.boosts?.spa === 'number' ? pokemon.boosts.spa : 0, + spd: typeof pokemon?.boosts?.spd === 'number' ? pokemon.boosts.spd : 0, + spe: typeof pokemon?.boosts?.spe === 'number' ? pokemon.boosts.spe : 0, + }, + + dirtyBoosts: { + atk: ('dirtyBoosts' in pokemon && pokemon.dirtyBoosts?.atk) || undefined, + def: ('dirtyBoosts' in pokemon && pokemon.dirtyBoosts?.def) || undefined, + spa: ('dirtyBoosts' in pokemon && pokemon.dirtyBoosts?.spa) || undefined, + spd: ('dirtyBoosts' in pokemon && pokemon.dirtyBoosts?.spd) || undefined, + spe: ('dirtyBoosts' in pokemon && pokemon.dirtyBoosts?.spe) || undefined, + }, + + status: pokemon?.status, + statusData: { + sleepTurns: pokemon?.statusData?.sleepTurns || 0, + toxicTurns: pokemon?.statusData?.toxicTurns || 0, + }, + + volatiles: pokemon?.volatiles, + turnstatuses: pokemon?.turnstatuses, + toxicCounter: pokemon?.statusData?.toxicTurns, + + hp: pokemon?.hp || 0, + maxhp: pokemon?.maxhp || 1, + fainted: pokemon?.fainted || !pokemon?.hp, + + moves: pokemon?.moves || [], + altMoves: ('altMoves' in pokemon && pokemon.altMoves) || [], + useUltimateMoves: ('useUltimateMoves' in pokemon && pokemon.useUltimateMoves) || false, + lastMove: pokemon?.lastMove, + moveTrack: Array.isArray(pokemon?.moveTrack) ? + // since pokemon.moveTrack is an array of arrays, + // we don't want to reference the original inner array elements + JSON.parse(JSON.stringify(pokemon?.moveTrack)) : + [], + moveState: { + revealed: ('moveState' in pokemon && pokemon.moveState?.revealed) || [], + learnset: ('moveState' in pokemon && pokemon.moveState?.learnset) || [], + other: ('moveState' in pokemon && pokemon.moveState?.other) || [], + }, + + criticalHit: ('criticalHit' in pokemon && pokemon.criticalHit) || false, + + preset: ('preset' in pokemon && pokemon.preset) || null, + presets: ('presets' in pokemon && pokemon.presets) || [], + autoPreset: 'autoPreset' in pokemon ? pokemon.autoPreset : true, + }; + + // fill in additional info if the Dex global is available (should be) + if (typeof Dex?.species?.get === 'function') { + const species = Dex.species.get(sanitizedPokemon.speciesForme); + + // don't really care if species is falsy here + sanitizedPokemon.baseStats = { + ...species?.baseStats, + }; + + // only update the types if the dex returned types + if (Array.isArray(species?.types) && species.types.length) { + sanitizedPokemon.types = [ + ...( species.types), + ]; + } + + // only update the abilities if the dex returned abilities + if (!sanitizedPokemon.abilities.length && Object.keys(species?.abilities || {}).length) { + sanitizedPokemon.abilities = [ + ...( Object.values(species.abilities)), + ]; + + if (!sanitizedPokemon.ability) { + [sanitizedPokemon.ability] = sanitizedPokemon.abilities; + } + } + } + + // remove any non-string volatiles + // (particularly Ditto's `transformed` volatile, which references an existing Pokemon object as its value) + Object.entries(sanitizedPokemon.volatiles || {}).forEach(([key, value]) => { + if (!['string', 'number'].includes(typeof value)) { + delete sanitizedPokemon.volatiles[key]; + } + }); + + if (!sanitizedPokemon?.calcdexId) { + sanitizedPokemon.calcdexId = calcPokemonCalcdexId(sanitizedPokemon); + } + + sanitizedPokemon.calcdexNonce = calcPokemonCalcdexNonce(sanitizedPokemon); + + return sanitizedPokemon; +}; diff --git a/src/pages/Calcdex/syncField.ts b/src/utils/battle/syncField.ts similarity index 94% rename from src/pages/Calcdex/syncField.ts rename to src/utils/battle/syncField.ts index 12e56bf7..c35999ea 100644 --- a/src/pages/Calcdex/syncField.ts +++ b/src/utils/battle/syncField.ts @@ -1,8 +1,8 @@ import { logger } from '@showdex/utils/debug'; -import type { CalcdexBattleField } from './CalcdexReducer'; +import type { CalcdexBattleField } from '@showdex/redux/store'; import { sanitizeField } from './sanitizeField'; -const l = logger('@showdex/pages/Calcdex/syncField'); +const l = logger('@showdex/utils/battle/syncField'); export const syncField = ( field: CalcdexBattleField, @@ -76,7 +76,7 @@ export const syncField = ( }); if (!didChange) { - return null; + return field; } return newField; diff --git a/src/utils/battle/syncPokemon.ts b/src/utils/battle/syncPokemon.ts new file mode 100644 index 00000000..f8c0636b --- /dev/null +++ b/src/utils/battle/syncPokemon.ts @@ -0,0 +1,324 @@ +import { PokemonBoostNames } from '@showdex/consts'; +import { calcPresetCalcdexId, guessServerSpread } from '@showdex/utils/calc'; +import { logger } from '@showdex/utils/debug'; +import type { + // AbilityName, + Generation, + // ItemName, + // MoveName, +} from '@pkmn/data'; +import type { + CalcdexMoveState, + CalcdexPokemon, + CalcdexPokemonPreset, +} from '@showdex/redux/store'; +import { sanitizePokemon } from './sanitizePokemon'; + +const l = logger('@showdex/utils/battle/syncPokemon'); + +export const syncPokemonBoosts = ( + pokemon: CalcdexPokemon, + clientPokemon: DeepPartial, +): CalcdexPokemon['boosts'] => { + const newPokemon: CalcdexPokemon = { ...pokemon }; + + l.debug( + 'syncPokemonBoosts()', + '\n', 'pokemon', pokemon, + '\n', 'clientPokemon', clientPokemon, + ); + + const boosts = PokemonBoostNames.reduce((prev, stat) => { + const currentValue = prev[stat]; + const value = clientPokemon?.boosts?.[stat] || 0; + + // l.debug( + // 'syncPokemonBoosts()', + // '\n', 'comparing stat', stat, 'currentValue', currentValue, 'with value', value, + // '\n', 'newPokemon', newPokemon?.ident, newPokemon, + // ); + + // l.debug(pokemon.ident, 'comparing value', value, 'and currentValue', currentValue, 'for stat', stat); + + if (value === currentValue) { + return prev; + } + + prev[stat] = value; + + return prev; + }, { + atk: newPokemon?.boosts?.atk || 0, + def: newPokemon?.boosts?.def || 0, + spa: newPokemon?.boosts?.spa || 0, + spd: newPokemon?.boosts?.spd || 0, + spe: newPokemon?.boosts?.spe || 0, + }); + + l.debug( + 'syncPokemonBoosts() -> return boosts', + '\n', 'boosts', boosts, + '\n', 'newPokemon', newPokemon?.ident, newPokemon, + ); + + return boosts; +}; + +export const syncPokemon = ( + pokemon: CalcdexPokemon, + clientPokemon: DeepPartial, + serverPokemon?: DeepPartial, + dex?: Generation, + format?: string, +): CalcdexPokemon => { + const newPokemon: CalcdexPokemon = { ...pokemon }; + + (<(keyof Showdown.Pokemon)[]> [ + 'name', + 'speciesForme', + // 'rawSpeciesForme', + 'hp', + 'maxhp', + 'status', + 'statusData', + 'ability', + 'baseAbility', + // 'nature', + 'item', + 'itemEffect', + 'prevItem', + 'prevItemEffect', + 'moveTrack', + 'volatiles', + // 'abilityToggled', // should be after volatiles + 'turnstatuses', + 'boosts', + ]).forEach((key) => { + const currentValue = newPokemon[ key]; // `newPokemon` is the final synced Pokemon that will be returned at the end + let value = clientPokemon?.[key]; // `clientPokemon` is what was changed and may not be a full Pokemon object + + if (value === undefined) { + return; + } + + switch (key) { + case 'ability': { + if (!value) { + return; + } + + if (value === newPokemon.dirtyAbility) { + newPokemon.dirtyAbility = null; + } + + break; + } + + case 'item': { + // ignore any unrevealed item (resulting in a falsy value) that hasn't been knocked-off/consumed/etc. + // (this can be checked since when the item be consumed, prevItem would NOT be falsy) + if ((!value || value === '(exists)') && !clientPokemon?.prevItem) { + return; + } + + // clear the dirtyItem if it's what the Pokemon actually has + // (otherwise, if the item hasn't been revealed yet, `value` would be falsy, + // but that's ok cause we have dirtyItem, i.e., no worries about clearing the user's input) + if (value === newPokemon.dirtyItem) { + newPokemon.dirtyItem = null; + } + + break; + } + + case 'prevItem': { + // check if the item was knocked-off and is the same as dirtyItem + // if so, clear the dirtyItem + // (note that `value` here is prevItem, NOT item!) + if (clientPokemon?.prevItemEffect === 'knocked off' && value === newPokemon.dirtyItem) { + newPokemon.dirtyItem = null; + } + + break; + } + + case 'speciesForme': { + if (clientPokemon?.volatiles?.formechange?.[1]) { + [, value] = clientPokemon.volatiles.formechange; + } + + /** @todo */ + // value = sanitizeSpeciesForme( value); + + break; + } + + // case 'rawSpeciesForme': { + // value = clientPokemon?.speciesForme ?? newPokemon?.rawSpeciesForme ?? newPokemon?.speciesForme; + // + // if (!value) { + // return; + // } + // + // break; + // } + + case 'boosts': { + value = syncPokemonBoosts(newPokemon, clientPokemon); + + break; + } + + case 'moves': { + if (!( value)?.length) { + return; + } + + break; + } + + case 'moveTrack': { + // l.debug('clientPokemon.moveTrack', clientPokemon?.moveTrack); + + if (clientPokemon?.moveTrack?.length) { + newPokemon.moveState = { + ...newPokemon.moveState, + revealed: clientPokemon.moveTrack.map((track) => track?.[0]), + }; + + // l.debug('value of type CalcdexMoveState set to', newPokemon.moveState); + } + + break; + } + + // case 'presets': { + // if (!Array.isArray(value) || !value.length) { + // value = currentValue; + // } + // + // break; + // } + + // case 'abilityToggled': { + // value = detectToggledAbility(clientPokemon); + // + // break; + // } + + default: break; + } + + /** @todo this line breaks when Ditto transforms since `volatiles.transformed[1]` is `Showdown.Pokemon` (NOT `string`) */ + if (JSON.stringify(value) === JSON.stringify(currentValue)) { // kekw + return; + } + + /** @see https://github.com/microsoft/TypeScript/issues/31663#issuecomment-518603958 */ + (newPokemon as Record)[key] = JSON.parse(JSON.stringify(value)); + }); + + // only using sanitizePokemon() to get some values back + const sanitizedPokemon = sanitizePokemon(newPokemon); + + // update some info if the Pokemon's speciesForme changed + // (since moveState requires async, we update that in syncBattle()) + if (pokemon.speciesForme !== newPokemon.speciesForme) { + newPokemon.baseStats = { ...sanitizedPokemon.baseStats }; + newPokemon.types = sanitizedPokemon.types; + newPokemon.ability = sanitizedPokemon.ability; + newPokemon.dirtyAbility = null; + newPokemon.abilities = sanitizedPokemon.abilities; + } + + newPokemon.abilityToggled = sanitizedPokemon.abilityToggled; + + // fill in some additional fields if the serverPokemon was provided + if (serverPokemon) { + newPokemon.serverSourced = true; + + if (serverPokemon.ability) { + const dexAbility = dex.abilities.get(serverPokemon.ability); + + if (dexAbility?.name) { + newPokemon.ability = dexAbility.name; + newPokemon.dirtyAbility = null; + } + } + + if (serverPokemon.item) { + const dexItem = dex.items.get(serverPokemon.item); + + if (dexItem?.name) { + newPokemon.item = dexItem.name; + newPokemon.dirtyItem = null; + } + } + + // build a preset around the serverPokemon + const guessedSpread = guessServerSpread( + dex, + newPokemon, + serverPokemon, + format?.includes('random') ? 'Hardy' : undefined, + ); + + const serverPreset: CalcdexPokemonPreset = { + name: 'Yours', + gen: dex.num, + format, + speciesForme: newPokemon.speciesForme || serverPokemon.speciesForme, + level: newPokemon.level || serverPokemon.level, + gender: newPokemon.gender || serverPokemon.gender || null, + ability: newPokemon.ability, + item: newPokemon.item, + ...guessedSpread, + }; + + newPokemon.nature = serverPreset.nature; + newPokemon.ivs = { ...serverPreset.ivs }; + newPokemon.evs = { ...serverPreset.evs }; + + // need to do some special processing for moves + // e.g., serverPokemon.moves = ['calmmind', 'moonblast', 'flamethrower', 'thunderbolt'] + // what we want: ['Calm Mind', 'Moonblast', 'Flamethrower', 'Thunderbolt'] + if (serverPokemon.moves?.length) { + serverPreset.moves = serverPokemon.moves.map((moveName) => { + const dexMove = dex.moves.get(moveName); + + if (!dexMove?.name) { + return null; + } + + return dexMove.name; + }).filter(Boolean); + + newPokemon.moves = [...serverPreset.moves]; + } + + serverPreset.calcdexId = calcPresetCalcdexId(serverPreset); + + const serverPresetIndex = newPokemon.presets.findIndex((p) => p.calcdexId === serverPreset.calcdexId); + + if (serverPresetIndex > -1) { + newPokemon.presets[serverPresetIndex] = serverPreset; + } else { + newPokemon.presets.unshift(serverPreset); + } + + // disabling autoPreset since we already set the preset here + // (also tells PokeInfo not to apply the first preset) + newPokemon.preset = serverPreset.calcdexId; + newPokemon.autoPreset = false; + } + + // const calcdexId = calcPokemonCalcdexId(newPokemon); + + // if (!newPokemon?.calcdexId || newPokemon.calcdexId !== calcdexId) { + // newPokemon.calcdexId = calcdexId; + // } + + newPokemon.calcdexNonce = sanitizedPokemon.calcdexNonce; + + return newPokemon; +}; From 3b223286a70515d2ade03d89fc4423fd9c64156b Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Fri, 12 Aug 2022 23:08:35 -0700 Subject: [PATCH 012/143] refac(calcdex): cleaned up and moved some more calcdex utils --- src/pages/Calcdex/calcPokemonHp.ts | 5 -- src/pages/Calcdex/detectGenFromFormat.ts | 20 ----- src/pages/Calcdex/detectPlayerKey.ts | 38 --------- src/pages/Calcdex/detectPokemonIdent.ts | 18 ----- src/pages/Calcdex/detectSpeciesForme.ts | 15 ---- src/pages/Calcdex/detectStatBoostDelta.ts | 66 --------------- src/pages/Calcdex/detectToggledAbility.ts | 44 ---------- src/pages/Calcdex/formatStatBoost.ts | 10 --- .../Calcdex => utils/calc}/calcCalcdexId.ts | 54 ++++++++----- .../calc}/calcCalcdexNonce.ts | 5 +- src/utils/calc/calcPokemonHp.ts | 5 ++ .../calc}/calcPokemonStats.ts | 33 +++++++- .../calc}/calcSmogonMatchup.ts | 81 +++++++++++++------ .../calc}/createSmogonField.ts | 2 +- .../calc}/createSmogonMove.ts | 6 +- .../calc}/createSmogonPokemon.ts | 38 +++++---- .../calc}/guessServerSpread.ts | 72 ++++++++--------- src/utils/calc/index.ts | 9 +++ 18 files changed, 195 insertions(+), 326 deletions(-) delete mode 100644 src/pages/Calcdex/calcPokemonHp.ts delete mode 100644 src/pages/Calcdex/detectGenFromFormat.ts delete mode 100644 src/pages/Calcdex/detectPlayerKey.ts delete mode 100644 src/pages/Calcdex/detectPokemonIdent.ts delete mode 100644 src/pages/Calcdex/detectSpeciesForme.ts delete mode 100644 src/pages/Calcdex/detectStatBoostDelta.ts delete mode 100644 src/pages/Calcdex/detectToggledAbility.ts delete mode 100644 src/pages/Calcdex/formatStatBoost.ts rename src/{pages/Calcdex => utils/calc}/calcCalcdexId.ts (54%) rename src/{pages/Calcdex => utils/calc}/calcCalcdexNonce.ts (95%) create mode 100644 src/utils/calc/calcPokemonHp.ts rename src/{pages/Calcdex => utils/calc}/calcPokemonStats.ts (85%) rename src/{pages/Calcdex => utils/calc}/calcSmogonMatchup.ts (66%) rename src/{pages/Calcdex => utils/calc}/createSmogonField.ts (85%) rename src/{pages/Calcdex => utils/calc}/createSmogonMove.ts (76%) rename src/{pages/Calcdex => utils/calc}/createSmogonPokemon.ts (82%) rename src/{pages/Calcdex => utils/calc}/guessServerSpread.ts (74%) create mode 100644 src/utils/calc/index.ts diff --git a/src/pages/Calcdex/calcPokemonHp.ts b/src/pages/Calcdex/calcPokemonHp.ts deleted file mode 100644 index 8118aa25..00000000 --- a/src/pages/Calcdex/calcPokemonHp.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { CalcdexPokemon } from './CalcdexReducer'; - -export const calcPokemonHp = ( - pokemon: Partial, -): CalcdexPokemon['hp'] => (pokemon?.hp || 0) / (pokemon?.maxhp || 1); diff --git a/src/pages/Calcdex/detectGenFromFormat.ts b/src/pages/Calcdex/detectGenFromFormat.ts deleted file mode 100644 index 09e1b08f..00000000 --- a/src/pages/Calcdex/detectGenFromFormat.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { GenerationNum } from '@pkmn/data'; - -const genRegex = /^gen(\d+)/i; -const defaultGen: GenerationNum = 8; - -export const detectGenFromFormat = ( - format: string, -): GenerationNum => { - if (!genRegex.test(format)) { - return defaultGen; - } - - const gen = parseInt(format.match(genRegex)[1], 10); - - if (gen < 1) { - return defaultGen; - } - - return gen; -}; diff --git a/src/pages/Calcdex/detectPlayerKey.ts b/src/pages/Calcdex/detectPlayerKey.ts deleted file mode 100644 index 7e6579de..00000000 --- a/src/pages/Calcdex/detectPlayerKey.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { CalcdexPlayerKey, CalcdexPokemon } from './CalcdexReducer'; -import { detectPokemonIdent } from './detectPokemonIdent'; - -export const detectPlayerKeyFromPokemon = ( - pokemon: Partial, -): CalcdexPlayerKey => { - const ident = detectPokemonIdent(pokemon); - - if (ident && /^p\d+:/.test(ident)) { - return ident.slice(0, ident.indexOf(':')); - } - - return null; -}; - -export const detectPlayerKeyFromBattle = ( - battle: Showdown.Battle, -): CalcdexPlayerKey => { - if (battle?.mySide?.sideid) { - // console.log('battle.mySide.sideid', battle.mySide.sideid, '\n', 'mySide', { - // sideid: battle.mySide.sideid, - // name: battle.mySide.name, - // myPokemon: battle.myPokemon, - // }); - - return battle.mySide.sideid; - } - - if (!battle?.myPokemon?.[0]) { - return null; - } - - const [firstPokemon] = battle.myPokemon; - - // console.log('firstPokemon', firstPokemon); - - return detectPlayerKeyFromPokemon(firstPokemon); -}; diff --git a/src/pages/Calcdex/detectPokemonIdent.ts b/src/pages/Calcdex/detectPokemonIdent.ts deleted file mode 100644 index 0177ed3e..00000000 --- a/src/pages/Calcdex/detectPokemonIdent.ts +++ /dev/null @@ -1,18 +0,0 @@ -// import type { CalcdexPokemon } from './CalcdexReducer'; -import { sanitizeSpeciesForme } from './sanitizeSpeciesForme'; - -export const detectPokemonIdent = ( - pokemon: Partial, -): string => sanitizeSpeciesForme( - [ - pokemon?.ident?.split?.(':')[0] - || pokemon?.side?.sideid, - pokemon?.volatiles?.formechange?.[1] - || pokemon?.speciesForme - || pokemon?.ident?.split?.(': ')[1] - || pokemon?.details?.split?.(', ')?.[0] - || pokemon?.name, - ].filter(Boolean).join(': ') - || pokemon?.ident - || pokemon?.searchid?.split?.('|')[0], -); diff --git a/src/pages/Calcdex/detectSpeciesForme.ts b/src/pages/Calcdex/detectSpeciesForme.ts deleted file mode 100644 index ffcb1141..00000000 --- a/src/pages/Calcdex/detectSpeciesForme.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { CalcdexPokemon } from './CalcdexReducer'; -import { detectPokemonIdent } from './detectPokemonIdent'; -// import { sanitizeSpeciesForme } from './sanitizeSpeciesForme'; - -/* eslint-disable arrow-body-style */ - -export const detectSpeciesForme = ( - pokemon: Partial, -): CalcdexPokemon['speciesForme'] => { - // if ('speciesForme' in (pokemon || {})) { - // return sanitizeSpeciesForme(pokemon.speciesForme); - // } - - return detectPokemonIdent(pokemon)?.split?.(': ')?.[1]; -}; diff --git a/src/pages/Calcdex/detectStatBoostDelta.ts b/src/pages/Calcdex/detectStatBoostDelta.ts deleted file mode 100644 index ebc0ea2b..00000000 --- a/src/pages/Calcdex/detectStatBoostDelta.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { CalcdexPokemon } from './CalcdexReducer'; - -export type PokemonStatBoostDelta = - | 'positive' - | 'negative'; - -/** - * Determines the direction of the delta of the Pokemon's stat boost. - * - * * In other words, checks whether the Pokemon's current stats are higher or lower than its base stats. - * * In cases where it's neither (i.e., no boosts were applied to the passed-in `stat`), `null` will be returned. - * - * @since 0.1.2 - */ -export const detectStatBoostDelta = ( - pokemon: CalcdexPokemon, - stat: Showdown.StatName, -): PokemonStatBoostDelta => { - if (stat === 'hp') { - return null; - } - - // check for boosts from abilities - if ('slowstart' in (pokemon?.volatiles || {}) && pokemon?.abilityToggled) { - if (['atk', 'spe'].includes(stat)) { - return 'negative'; - } - } - - // check for status-dependent boosts from abilities - const abilitySearchString = pokemon?.ability?.toLowerCase?.(); - const hasGuts = abilitySearchString === 'guts'; - const hasQuickFeet = abilitySearchString === 'quick feet'; - - if (pokemon?.status && pokemon.status !== '???') { - if (hasGuts && stat === 'atk') { - return 'positive'; - } - - if (hasQuickFeet && stat === 'spe') { - return 'positive'; - } - - // may be problematic since we're not using the Pokemon's base stats, - // but oh well, this ok for now lmaoo - if (pokemon.status === 'brn' && stat === 'atk') { - return 'negative'; - } - - if (pokemon.status === 'par' && stat === 'spe') { - return 'negative'; - } - } - - const boost = pokemon?.dirtyBoosts?.[stat] ?? pokemon?.boosts?.[stat] ?? 0; - - if (boost > 0) { - return 'positive'; - } - - if (boost < 0) { - return 'negative'; - } - - return null; -}; diff --git a/src/pages/Calcdex/detectToggledAbility.ts b/src/pages/Calcdex/detectToggledAbility.ts deleted file mode 100644 index 47713cbe..00000000 --- a/src/pages/Calcdex/detectToggledAbility.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { PokemonToggleAbilities } from '@showdex/consts'; -import type { CalcdexPokemon } from './CalcdexReducer'; -import { calcPokemonHp } from './calcPokemonHp'; - -/** - * Determines whether the Pokemon's toggleable ability is active (if applicable). - * - * * Primarily depends on the Pokemon's `volatiles` object (from the `battle` state). - * - For instance, if Heatran's *Flash Fire* ability is activated, you'll see its `volatiles` object - * set to `{ flashfire: ['flashfire'] }` (at the time of writing this, of course). - * - Otherwise, its `ability` would still be `'Flash Fire'`, but `volatiles` would be an empty object, i.e., `{}`. - * * Only exception is the *Multiscale* ability, which is only active when the Pokemon's HP is at 100%, - * similar to how the *Focus Sash* item works. - * - Pretty sure `calculate()` from `@smogon/calc` doesn't care whether `abilityOn` (of `SmogonPokemon`) - * is `true` or `false`, but we keep track of it for visual purposes. - * - (Side note: `SmogonPokemon` mentioned above is an alias of the `Pokemon` class from `@smogon/calc`.) - * - Pokemon's HP value isn't currently editable, so there's no way to toggle *Multiscale* on/off - * (before the Pokemon takes any damage). - * - Additionally, if, say, Dragonite takes damage, this would return `false`, since its HP is no longer at 100%. - * In the case where Dragonite uses *Roost* and the opponent doesn't attack (e.g., switches out), - * resulting in Dragonite healing back to full health (i.e., 100%), this would return `true` again. - * - * @returns `true` if the Pokemon's ability is *toggleable* and *active*, `false` otherwise. - * @since 0.1.2 - */ -export const detectToggledAbility = ( - pokemon: Partial, -): boolean => { - const ability = pokemon?.ability; - const hasMultiscale = ability?.toLowerCase?.() === 'multiscale'; - const toggleAbility = !hasMultiscale && PokemonToggleAbilities.includes(ability); - - if (hasMultiscale) { - return calcPokemonHp(pokemon) === 1; - } - - if (toggleAbility) { - return Object.keys(pokemon?.volatiles ?? {}).includes( - ability?.replace?.(/\s/g, '').toLowerCase(), - ); - } - - return false; -}; diff --git a/src/pages/Calcdex/formatStatBoost.ts b/src/pages/Calcdex/formatStatBoost.ts deleted file mode 100644 index 211928f0..00000000 --- a/src/pages/Calcdex/formatStatBoost.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const formatStatBoost = (boostedStat: number): string => { - if (typeof boostedStat !== 'number') { - return ''; - } - - // otherwise, something like '50.400000000000006' can get rendered lol - const isFloat = /\.\d+$/.test(boostedStat.toString()); - - return boostedStat.toFixed(isFloat ? 1 : 0); -}; diff --git a/src/pages/Calcdex/calcCalcdexId.ts b/src/utils/calc/calcCalcdexId.ts similarity index 54% rename from src/pages/Calcdex/calcCalcdexId.ts rename to src/utils/calc/calcCalcdexId.ts index 83e6d0e2..2336c4e1 100644 --- a/src/pages/Calcdex/calcCalcdexId.ts +++ b/src/utils/calc/calcCalcdexId.ts @@ -1,5 +1,7 @@ import { NIL as NIL_UUID, v5 as uuidv5 } from 'uuid'; -import type { CalcdexPokemon, CalcdexPokemonPreset } from './CalcdexReducer'; +import { env } from '@showdex/utils/core'; +import { detectPlayerKeyFromPokemon } from '@showdex/utils/battle'; +import type { CalcdexPokemon, CalcdexPokemonPreset } from '@showdex/redux/store'; export const serializePayload = (payload: T): string => Object.entries(payload || {}) .map(([key, value]) => `${key}:${value?.toString?.() ?? 'undefined'}`) @@ -24,35 +26,49 @@ export const calcCalcdexId = (payload: T): string => { return null; } - return uuidv5(serialized, process.env.UUID_NAMESPACE || NIL_UUID); + return uuidv5(serialized, env('uuid-namespace', NIL_UUID)); }; export const calcPresetCalcdexId = ( preset: CalcdexPokemonPreset, ): string => calcCalcdexId>>({ name: preset?.name, - species: preset?.species, - level: preset?.level?.toString(), - shiny: preset?.shiny?.toString(), - ability: preset?.ability, - item: preset?.item, - moves: preset?.moves?.join(','), - nature: preset?.nature, - ivs: calcCalcdexId>(preset?.ivs), - evs: calcCalcdexId>(preset?.evs), - happiness: preset?.happiness?.toString(), - pokeball: preset?.pokeball, - hpType: preset?.hpType, - gigantamax: preset?.gigantamax?.toString(), + gen: String(preset?.gen), + format: preset?.format, + speciesForme: preset?.speciesForme, + level: String(preset?.level), + // shiny: String(preset?.shiny), + // ability: preset?.ability, + // altAbilities: preset?.altAbilities?.join(','), + // nature: preset?.nature, + // item: preset?.item, + // altItems: preset?.altItems?.join(','), + // moves: preset?.moves?.join(','), + // altMoves: preset?.moves?.join(','), + // ivs: calcCalcdexId(preset?.ivs), + // evs: calcCalcdexId(preset?.evs), + // happiness: String(preset?.happiness), + // pokeball: preset?.pokeball, + // hpType: preset?.hpType, + // gigantamax: String(preset?.gigantamax), }); export const calcPokemonCalcdexId = ( - pokemon: Partial, + pokemon: DeepPartial | DeepPartial | DeepPartial = {}, ): string => calcCalcdexId>>({ - ident: pokemon?.ident, - // name: pokemon?.name, - level: pokemon?.level?.toString(), + // ident: pokemon?.ident, + + name: [ + detectPlayerKeyFromPokemon(pokemon), + // pokemon?.name?.replace(/-.+$/, ''), // 'Ho-Oh' -> 'Ho' ? LOL + 'slot' in pokemon && typeof pokemon.slot === 'number' ? + String(pokemon.slot) : + pokemon?.speciesForme?.replace(/-.+$/, ''), + ].filter(Boolean).join(': '), + + level: String(pokemon?.level), gender: pokemon?.gender, + // shiny: String(pokemon?.shiny), // bad idea, subject to change mid-battle }); export const calcSideCalcdexId = ( diff --git a/src/pages/Calcdex/calcCalcdexNonce.ts b/src/utils/calc/calcCalcdexNonce.ts similarity index 95% rename from src/pages/Calcdex/calcCalcdexNonce.ts rename to src/utils/calc/calcCalcdexNonce.ts index 8b2c5352..9bf2f27b 100644 --- a/src/pages/Calcdex/calcCalcdexNonce.ts +++ b/src/utils/calc/calcCalcdexNonce.ts @@ -1,5 +1,6 @@ import { NIL as NIL_UUID, v5 as uuidv5 } from 'uuid'; -import type { CalcdexPokemon } from './CalcdexReducer'; +import { env } from '@showdex/utils/core'; +import type { CalcdexPokemon } from '@showdex/redux/store'; import { calcCalcdexId } from './calcCalcdexId'; export const calcPokemonCalcdexNonce = ( @@ -83,6 +84,6 @@ export const calcBattleCalcdexNonce = ( p4: calcSideCalcdexNonce(battle?.p4), currentStep: battle?.currentStep?.toString(), stepQueue: Array.isArray(battle?.stepQueue) && battle.stepQueue.length ? - uuidv5(battle.stepQueue.join('|'), process.env.UUID_NAMESPACE || NIL_UUID) : + uuidv5(battle.stepQueue.join('|'), env('uuid-namespace', NIL_UUID)) : null, }); diff --git a/src/utils/calc/calcPokemonHp.ts b/src/utils/calc/calcPokemonHp.ts new file mode 100644 index 00000000..7dd27c16 --- /dev/null +++ b/src/utils/calc/calcPokemonHp.ts @@ -0,0 +1,5 @@ +import type { CalcdexPokemon } from '@showdex/redux/store'; + +export const calcPokemonHp = ( + pokemon: DeepPartial | DeepPartial, +): CalcdexPokemon['hp'] => (pokemon?.hp || 0) / (pokemon?.maxhp || 1); diff --git a/src/pages/Calcdex/calcPokemonStats.ts b/src/utils/calc/calcPokemonStats.ts similarity index 85% rename from src/pages/Calcdex/calcPokemonStats.ts rename to src/utils/calc/calcPokemonStats.ts index 9ed56e22..424b6275 100644 --- a/src/pages/Calcdex/calcPokemonStats.ts +++ b/src/utils/calc/calcPokemonStats.ts @@ -1,8 +1,9 @@ import { PokemonStatNames } from '@showdex/consts'; +import { detectSpeciesForme } from '@showdex/utils/battle'; import { logger } from '@showdex/utils/debug'; import type { Generation } from '@pkmn/data'; -import type { CalcdexPokemon } from './CalcdexReducer'; -import { detectSpeciesForme } from './detectSpeciesForme'; +import type { CalcdexPokemon } from '@showdex/redux/store'; +import { calcPokemonHp } from './calcPokemonHp'; const l = logger('@showdex/pages/Calcdex/calcPokemonStats'); @@ -15,9 +16,20 @@ const initialStats: CalcdexPokemon['calculatedStats'] = { spe: 0, }; +/** + * @todo Flower Gift abililty (factor in dmax). + * @todo Fur Coat ability. + * @todo Choice Band/Scarf/Specs (factor in dmax). + * @todo Gorilla Tactics ability (factor in dmax). + * @todo Grass Pelt ability. + * @todo Huge Power ability. + * @todo Hustle ability. + * @todo Marvel Scale ability. + * @since 0.1.0 + */ export const calcPokemonStats = ( dex: Generation, - pokemon: Partial, + pokemon: DeepPartial, ): CalcdexPokemon['calculatedStats'] => { if (typeof dex?.stats?.calc !== 'function' || typeof dex?.species?.get !== 'function') { l.warn( @@ -52,6 +64,7 @@ export const calcPokemonStats = ( const gen = dex.num; const hasGuts = pokemon?.ability?.toLowerCase?.() === 'guts'; const hasQuickFeet = pokemon?.ability?.toLowerCase?.() === 'quick feet'; + const hasDefeatist = pokemon?.ability?.toLowerCase() === 'defeatist'; // rebuild the Pokemon's base stats to make sure all values are available const baseStats: CalcdexPokemon['baseStats'] = { @@ -109,6 +122,11 @@ export const calcPokemonStats = ( nature, ); + // if the Pokemon is Zygarde-Complete, double its HP + if (speciesForme === 'Zygarde-Complete' && stat === 'hp') { + prev[stat] *= 2; + } + // re-calculate any boosted stat (except for HP, obviously) if (stat !== 'hp' && stat in boosts) { const stage = boosts[stat]; @@ -128,6 +146,15 @@ export const calcPokemonStats = ( } } + // handle boosts/reductions by the Pokemon's current HP + const hp = calcPokemonHp(pokemon); + + if (hasDefeatist && hp <= 0.5) { + if (['atk', 'spa'].includes(stat)) { + prev[stat] *= 0.5; + } + } + // handle boosts/reductions by the Pokemon's current status condition, if any if (pokemon?.status) { if (hasGuts) { diff --git a/src/pages/Calcdex/calcSmogonMatchup.ts b/src/utils/calc/calcSmogonMatchup.ts similarity index 66% rename from src/pages/Calcdex/calcSmogonMatchup.ts rename to src/utils/calc/calcSmogonMatchup.ts index e9d02935..e67ee55d 100644 --- a/src/pages/Calcdex/calcSmogonMatchup.ts +++ b/src/utils/calc/calcSmogonMatchup.ts @@ -1,14 +1,27 @@ import { calculate } from '@smogon/calc'; -// import { logger } from '@showdex/utils/debug'; -import type { GenerationNum } from '@pkmn/data'; +import { + createSmogonField, + createSmogonMove, + createSmogonPokemon, +} from '@showdex/utils/calc'; +import { logger } from '@showdex/utils/debug'; +import type { Generation, MoveName } from '@pkmn/data'; import type { - Field as SmogonField, + // Field as SmogonField, Move as SmogonMove, - Pokemon as SmogonPokemon, + // Pokemon as SmogonPokemon, Result, } from '@smogon/calc'; +import type { CalcdexBattleField, CalcdexPokemon } from '@showdex/redux/store'; export interface CalcdexMatchupResult { + /** + * Move that the calculator used to calculate the calculatable calculation. + * + * @since 0.1.3 + */ + move?: SmogonMove; + /** * In the format `XXX.X% - XXX.X%`, where `X` are numbers. * @@ -120,7 +133,7 @@ const getKoColor = ( return SmogonMatchupKoColors[koColorIndex]; }; -// const l = logger('@showdex/pages/Calcdex/calcSmogonMatchup'); +const l = logger('@showdex/utils/calc/calcSmogonMatchup'); /** * Verifies that the arguments look *decently* good, then yeets them to `calculate()` from `@smogon/calc`. @@ -129,35 +142,53 @@ const getKoColor = ( * @since 0.1.2 */ export const calcSmogonMatchup = ( - gen: GenerationNum, - playerPokemon: SmogonPokemon, - opponentPokemon: SmogonPokemon, - playerMove: SmogonMove, - field?: SmogonField, + dex: Generation, + playerPokemon: CalcdexPokemon, + opponentPokemon: CalcdexPokemon, + playerMove: MoveName, + field?: CalcdexBattleField, ): CalcdexMatchupResult => { - if (!gen || !playerPokemon?.name || !opponentPokemon?.name || !playerMove?.name) { + if (!dex?.num || !playerPokemon?.speciesForme || !opponentPokemon?.speciesForme || !playerMove) { + if (__DEV__ && playerMove) { + l.warn( + 'Calculation ignored due to invalid arguments', + '\n', 'dex.num', dex?.num, + '\n', 'playerPokemon.speciesForme', playerPokemon?.speciesForme, + '\n', 'opponentPokemon.speciesForme', opponentPokemon?.speciesForme, + '\n', 'playerMove', playerMove, + '\n', 'field', field, + '\n', '(You will only see this warning on development.)', + ); + } + return null; } + const smogonPlayerPokemon = createSmogonPokemon(dex, playerPokemon); + const smogonPlayerPokemonMove = createSmogonMove(dex.num, playerPokemon, playerMove); + const smogonOpponentPokemon = createSmogonPokemon(dex, opponentPokemon); + const smogonField = createSmogonField(field); + const result = calculate( - gen, - playerPokemon, - opponentPokemon, - playerMove, - field, + dex.num, + smogonPlayerPokemon, + smogonOpponentPokemon, + smogonPlayerPokemonMove, + smogonField, ); - // l.debug( - // 'calcSmogonMatchup() <- calculate()', - // '\n', 'result', result, - // '\n', 'gen', gen, - // '\n', 'playerPokemon', playerPokemon.name || '???', playerPokemon, - // '\n', 'opponentPokemon', opponentPokemon.name || '???', opponentPokemon, - // '\n', 'playerMove', playerMove.name || '???', playerMove, - // '\n', 'field', field, - // ); + l.debug( + 'calcSmogonMatchup() <- calculate()', + '\n', 'result', result, + '\n', 'dex.num', dex.num, + '\n', 'playerPokemon', playerPokemon.name || '???', playerPokemon, + '\n', 'opponentPokemon', opponentPokemon.name || '???', opponentPokemon, + '\n', 'playerMove', playerMove || '???', + '\n', 'field', field, + ); const matchup: CalcdexMatchupResult = { + move: smogonPlayerPokemonMove, damageRange: formatDamageRange(result), koChance: formatKoChance(result), koColor: getKoColor(result), diff --git a/src/pages/Calcdex/createSmogonField.ts b/src/utils/calc/createSmogonField.ts similarity index 85% rename from src/pages/Calcdex/createSmogonField.ts rename to src/utils/calc/createSmogonField.ts index 42b1da6a..7a4b116e 100644 --- a/src/pages/Calcdex/createSmogonField.ts +++ b/src/utils/calc/createSmogonField.ts @@ -1,5 +1,5 @@ import { Field as SmogonField } from '@smogon/calc'; -import type { CalcdexBattleField } from './CalcdexReducer'; +import type { CalcdexBattleField } from '@showdex/redux/store'; export const createSmogonField = ( field: CalcdexBattleField, diff --git a/src/pages/Calcdex/createSmogonMove.ts b/src/utils/calc/createSmogonMove.ts similarity index 76% rename from src/pages/Calcdex/createSmogonMove.ts rename to src/utils/calc/createSmogonMove.ts index 4cad668a..c5e0f002 100644 --- a/src/pages/Calcdex/createSmogonMove.ts +++ b/src/utils/calc/createSmogonMove.ts @@ -1,18 +1,18 @@ import { Move as SmogonMove } from '@smogon/calc'; import type { GenerationNum, MoveName } from '@pkmn/data'; -import type { CalcdexPokemon } from './CalcdexReducer'; +import type { CalcdexPokemon } from '@showdex/redux/store'; export const createSmogonMove = ( gen: GenerationNum, pokemon: CalcdexPokemon, moveName: MoveName, ): SmogonMove => { - if (!gen || !pokemon?.rawSpeciesForme || !moveName) { + if (!gen || !pokemon?.speciesForme || !moveName) { return null; } const smogonMove = new SmogonMove(gen, moveName, { - species: pokemon?.rawSpeciesForme, + species: pokemon?.rawSpeciesForme || pokemon?.speciesForme, ability: pokemon?.dirtyAbility ?? pokemon?.ability, item: pokemon?.dirtyItem ?? pokemon?.item, useZ: gen === 7 && pokemon?.useUltimateMoves, diff --git a/src/pages/Calcdex/createSmogonPokemon.ts b/src/utils/calc/createSmogonPokemon.ts similarity index 82% rename from src/pages/Calcdex/createSmogonPokemon.ts rename to src/utils/calc/createSmogonPokemon.ts index 86a030e8..54b7f6fe 100644 --- a/src/pages/Calcdex/createSmogonPokemon.ts +++ b/src/utils/calc/createSmogonPokemon.ts @@ -1,27 +1,25 @@ import { Pokemon as SmogonPokemon } from '@smogon/calc'; import { PokemonToggleAbilities } from '@showdex/consts'; +import { detectPokemonIdent, detectSpeciesForme } from '@showdex/utils/battle'; import { logger } from '@showdex/utils/debug'; import type { Generation } from '@pkmn/data'; -import type { GenerationNum, State as SmogonState } from '@smogon/calc'; -import type { CalcdexPokemon } from './CalcdexReducer'; +import type { State as SmogonState } from '@smogon/calc'; +import type { CalcdexPokemon } from '@showdex/redux/store'; import { calcPokemonHp } from './calcPokemonHp'; import { calcPokemonStats } from './calcPokemonStats'; -import { detectPokemonIdent } from './detectPokemonIdent'; -import { detectSpeciesForme } from './detectSpeciesForme'; const l = logger('@showdex/pages/Calcdex/createSmogonPokemon'); export const createSmogonPokemon = ( - gen: GenerationNum, dex: Generation, pokemon: CalcdexPokemon, ): SmogonPokemon => { - if (typeof gen !== 'number' || gen < 1) { - l.warn( - 'received an invalid gen value', - '\n', 'gen', gen, - '\n', 'pokemon', pokemon, - ); + if (typeof dex?.num !== 'number' || dex.num < 1) { + // l.warn( + // 'received an invalid gen value', + // '\n', 'gen', gen, + // '\n', 'pokemon', pokemon, + // ); return null; } @@ -29,13 +27,13 @@ export const createSmogonPokemon = ( const ident = detectPokemonIdent(pokemon); if (!ident) { - l.debug( - 'createSmogonPokemon() <- detectPokemonIdent()', - '\n', 'failed to detect Pokemon\'s ident', - '\n', 'ident', ident, - '\n', 'gen', gen, - '\n', 'pokemon', pokemon, - ); + // l.debug( + // 'createSmogonPokemon() <- detectPokemonIdent()', + // '\n', 'failed to detect Pokemon\'s ident', + // '\n', 'ident', ident, + // '\n', 'gen', gen, + // '\n', 'pokemon', pokemon, + // ); return null; } @@ -48,7 +46,7 @@ export const createSmogonPokemon = ( 'createSmogonPokemon() <- detectSpeciesForme()', '\n', 'failed to detect speciesForme from Pokemon with ident', ident, '\n', 'speciesForme', speciesForme, - '\n', 'gen', gen, + '\n', 'gen', dex.num, '\n', 'pokemon', pokemon, ); @@ -108,7 +106,7 @@ export const createSmogonPokemon = ( } const smogonPokemon = new SmogonPokemon( - gen, + dex.num, speciesForme === 'Aegislash' ? 'Aegislash-Blade' : speciesForme, // this hurts my soul options, ); diff --git a/src/pages/Calcdex/guessServerSpread.ts b/src/utils/calc/guessServerSpread.ts similarity index 74% rename from src/pages/Calcdex/guessServerSpread.ts rename to src/utils/calc/guessServerSpread.ts index 22d88269..178d57b8 100644 --- a/src/pages/Calcdex/guessServerSpread.ts +++ b/src/utils/calc/guessServerSpread.ts @@ -1,61 +1,54 @@ import { PokemonCommonNatures, PokemonStatNames } from '@showdex/consts'; import { logger } from '@showdex/utils/debug'; -import type { Generation } from '@pkmn/data'; -import type { CalcdexPokemon, CalcdexPokemonPreset } from './CalcdexReducer'; +import type { Generation, NatureName } from '@pkmn/data'; +import type { CalcdexPokemon, CalcdexPokemonPreset } from '@showdex/redux/store'; -const l = logger('@showdex/pages/Calcdex/guessServerSpread'); +const l = logger('@showdex/utils/calc/guessServerSpread'); /** * Attempts to guess the spread (nature/EVs/IVs) of the passed-in `ServerPokemon`. * - * * The client unfortunately only gives us the *final* stats after the spread has been applied. + * * Client unfortunately only gives us the *final* stats after the spread has been applied. * * This attempts to brute-force different values to obtain those final stats. + * - If you know the nature, you can specify it under `knownNature` to vastly improve the guesswork. + * - For instance, you can assume the `knownNature` to be `'Hardy'` if the `format` is randoms. + * - Note that other natures will still be checked if a matching spread couldn't be found + * from the provided `knownNature`. * * Probably one of the worst things I've ever written, but it works... kinda. * - There's this nasty part of the function with 4 nested `for` loops, resulting in `O(n^4)`. * - That's not even including the loops outside of this function! - * - Will occassionally guess the wrong nature with some Chinese EVs/IVs. - * - Apparently, there are more than one spread combination that can give the same final stats. + * * Will occassionally guess a strange nature with some Chinese EVs/IVs. + * - Apparently, there can be more than one spread that can produce the same final stats. * * @todo find a better way to implement or optimize this algorithm cause it's BAAAADDDD LOLOL * @since 0.1.1 */ export const guessServerSpread = ( dex: Generation, - clientPokemon: Partial, - serverPokemon: Showdown.ServerPokemon, + pokemon: CalcdexPokemon, + serverPokemon: DeepPartial, + knownNature?: NatureName, ): Partial => { - if (typeof dex?.species?.get !== 'function') { + if (!pokemon?.speciesForme) { l.warn( - 'received an invalid dex argument w/o dex.species.get()', - '\n', 'dex', dex, - '\n', 'clientPokemon', clientPokemon, - '\n', 'serverPokemon', serverPokemon, - ); - - return null; - } - - if (!clientPokemon?.speciesForme) { - l.warn( - 'received an invalid clientPokemon w/o a speciesForme', - '\n', 'dex', dex, - '\n', 'clientPokemon', clientPokemon, + 'received an invalid Pokemon without a speciesForme', + '\n', 'pokemon', pokemon, '\n', 'serverPokemon', serverPokemon, ); return null; } - const species = dex.species.get(clientPokemon.speciesForme); + const species = dex.species.get(pokemon.speciesForme); if (typeof species?.baseStats?.hp !== 'number') { l.warn( 'guessServerSpread() <- dex.species.get()', '\n', 'received no baseStats for the given speciesForme', - '\n', 'speciesForme', clientPokemon.speciesForme, + '\n', 'speciesForme', pokemon.speciesForme, '\n', 'species', species, '\n', 'dex', dex, - '\n', 'clientPokemon', clientPokemon, + '\n', 'pokemon', pokemon, '\n', 'serverPokemon', serverPokemon, ); @@ -85,16 +78,21 @@ export const guessServerSpread = ( // this is the spread that we'll return after guessing const guessedSpread: Partial = { - nature: null, + nature: knownNature || null, ivs: {}, evs: {}, }; + const natureCombinations = [ + guessedSpread.nature, + ...PokemonCommonNatures, + ].filter(Boolean); + // don't read any further... I'm warning you :o - for (const natureName of PokemonCommonNatures) { + for (const natureName of natureCombinations) { const nature = dex.natures.get(natureName); - // l.debug('trying nature', nature.name, 'for Pokemon', clientPokemon.ident); + // l.debug('trying nature', nature.name, 'for Pokemon', pokemon.ident); const calculatedStats: Showdown.StatsTable = { hp: 0, @@ -119,15 +117,15 @@ export const guessServerSpread = ( baseStats[stat], iv, ev, - clientPokemon.level, + pokemon.level, nature, ); // warning: if you don't filter this log, there will be lots of logs (and I mean A LOT) // may crash your browser depending on your computer's specs. debug at your own risk! - // if (clientPokemon.ident.endsWith('Clefable')) { + // if (pokemon.ident.endsWith('Clefable')) { // l.debug( - // 'trying to find the spread for', clientPokemon.ident, 'stat', stat, + // 'trying to find the spread for', pokemon.ident, 'stat', stat, // '\n', 'calculatedStat', calculatedStats[stat], 'knownStat', knownStats[stat], // '\n', 'iv', iv, 'ev', ev, // '\n', 'nature', nature.name, '+', nature.plus, '-', nature.minus, @@ -138,7 +136,7 @@ export const guessServerSpread = ( // this one isn't too bad to print, but will still cause a considerable slowdown // (you should only uncomment the debug log if shit is really hitting the fan) // l.debug( - // 'found matching combination for', clientPokemon.ident, 'stat', stat, + // 'found matching combination for', pokemon.ident, 'stat', stat, // '\n', 'calculatedStat', calculatedStats[stat], 'knownStat', knownStats[stat], // '\n', 'iv', iv, 'ev', ev, // '\n', 'nature', nature.name, '+', nature.plus, '-', nature.minus, @@ -172,12 +170,12 @@ export const guessServerSpread = ( if (sameStats && evsLegal) { // l.debug( - // 'found nature that matches all of the stats for Pokemon', clientPokemon.ident, + // 'found nature that matches all of the stats for Pokemon', pokemon.ident, // '\n', 'nature', nature.name, // '\n', 'calculatedStats', calculatedStats, // '\n', 'knownStats', knownStats, // '\n', 'dex', dex, - // '\n', 'clientPokemon', clientPokemon, + // '\n', 'pokemon', pokemon, // '\n', 'serverPokemon', serverPokemon, // ); @@ -193,10 +191,10 @@ export const guessServerSpread = ( l.debug( 'guessServerSpread() -> return guessedSpread', - '\n', 'returning the best guess of the spread for Pokemon', clientPokemon.ident, + '\n', 'returning the best guess of the spread for Pokemon', pokemon.ident, '\n', 'guessedSpread', guessedSpread, '\n', 'dex', dex, - '\n', 'clientPokemon', clientPokemon, + '\n', 'pokemon', pokemon, '\n', 'serverPokemon', serverPokemon, ); diff --git a/src/utils/calc/index.ts b/src/utils/calc/index.ts new file mode 100644 index 00000000..8ce7ecbb --- /dev/null +++ b/src/utils/calc/index.ts @@ -0,0 +1,9 @@ +export * from './calcCalcdexId'; +export * from './calcCalcdexNonce'; +export * from './calcPokemonHp'; +export * from './calcPokemonStats'; +export * from './calcSmogonMatchup'; +export * from './createSmogonField'; +export * from './createSmogonMove'; +export * from './createSmogonPokemon'; +export * from './guessServerSpread'; From c14d352a73ed740e1eecd0218616748b0b5ce60d Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Fri, 12 Aug 2022 23:09:10 -0700 Subject: [PATCH 013/143] refac(utils): replaced process.env w/ getEnv in open app utils --- src/utils/app/openShowdownUser.ts | 3 ++- src/utils/app/openSmogonUniversity.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/utils/app/openShowdownUser.ts b/src/utils/app/openShowdownUser.ts index c8d45c3a..874c99a9 100644 --- a/src/utils/app/openShowdownUser.ts +++ b/src/utils/app/openShowdownUser.ts @@ -1,3 +1,4 @@ +import { env } from '@showdex/utils/core'; import { logger } from '@showdex/utils/debug'; /** @@ -48,7 +49,7 @@ export const openShowdownUser = ( } const windowUrl = [ - process.env.SHOWDOWN_USERS_URL, + env('showdown-users-url'), name, ].filter(Boolean).join('/'); diff --git a/src/utils/app/openSmogonUniversity.ts b/src/utils/app/openSmogonUniversity.ts index 4282f220..aa78a7fe 100644 --- a/src/utils/app/openSmogonUniversity.ts +++ b/src/utils/app/openSmogonUniversity.ts @@ -1,4 +1,5 @@ import slugify from 'slugify'; +import { env } from '@showdex/utils/core'; import { logger } from '@showdex/utils/debug'; import type { GenerationNum } from '@pkmn/types'; @@ -150,7 +151,7 @@ export const openSmogonUniversity = ( } const windowUrl = [ - process.env.SMOGON_UNIVERSITY_DEX_URL, + env('smogon-university-dex-url'), genSlug, category, slugifiedName, From 53e3d626b03c18d9913f3e8967081bc6f1e9d599 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Fri, 12 Aug 2022 23:09:51 -0700 Subject: [PATCH 014/143] style(bg): changed nothing in the end lmao --- src/background.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/background.ts b/src/background.ts index 12b85c14..31bd4618 100644 --- a/src/background.ts +++ b/src/background.ts @@ -16,7 +16,11 @@ // } // }); -chrome.runtime.onMessageExternal.addListener((message: Record, _sender, sendResponse) => { +chrome.runtime.onMessageExternal.addListener(( + message: Record, + _sender, + sendResponse, +) => { switch ( message?.type) { case 'fetch': { void (async () => { @@ -32,6 +36,8 @@ chrome.runtime.onMessageExternal.addListener((message: Record, const json = await >> response.json(); + // console.log('response.json()', json); + sendResponse({ ok: response.ok, status: response.status, From 1a9f732241ed0e6192269d7de29c7e47b02b1a8d Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Fri, 12 Aug 2022 23:10:34 -0700 Subject: [PATCH 015/143] feat(main): added redux store to bootstrappers --- src/main.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main.ts b/src/main.ts index b049deac..23ad5422 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,13 +1,17 @@ import { bootstrap as bootstrapCalcdex } from '@showdex/pages'; +import { createStore } from '@showdex/redux/store'; import { logger } from '@showdex/utils/debug'; +import type { RootStore } from '@showdex/redux/store'; import '@showdex/styles/global.scss'; const l = logger('@showdex/main'); -const bootstrappers: ((roomId?: string) => void)[] = [ +const bootstrappers: ((store: RootStore, roomId?: string) => void)[] = [ bootstrapCalcdex, ]; +const store = createStore(); + // eslint-disable-next-line @typescript-eslint/unbound-method const { receive } = app || {}; @@ -31,7 +35,7 @@ app.receive = (data: string) => { ); // call each bootstrapper - bootstrappers.forEach((bootstrapper) => bootstrapper(roomId)); + bootstrappers.forEach((bootstrapper) => bootstrapper(store, roomId)); } // call the original function From 993c9fc4c9bbfb618b2ef65f0edaf50967448a96 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Fri, 12 Aug 2022 23:11:57 -0700 Subject: [PATCH 016/143] feat(calcdex): added new and improved presets hook maybe --- src/pages/Calcdex/usePresets.ts | 297 ++++++++++++++++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 src/pages/Calcdex/usePresets.ts diff --git a/src/pages/Calcdex/usePresets.ts b/src/pages/Calcdex/usePresets.ts new file mode 100644 index 00000000..552f9c43 --- /dev/null +++ b/src/pages/Calcdex/usePresets.ts @@ -0,0 +1,297 @@ +import * as React from 'react'; +// import { FormatLabels } from '@showdex/consts'; +import { usePokemonGensPresetQuery, usePokemonRandomsPresetQuery } from '@showdex/redux/services'; +import { detectGenFromFormat } from '@showdex/utils/battle'; +import { logger } from '@showdex/utils/debug'; +import type { CalcdexPokemonPreset } from '@showdex/redux/store'; + +export interface CalcdexPresetsHookOptions { + /** + * Format of the battle. + * + * * Can be derived from the `battleId`, which contains the format as the second element + * when `battleId` is `split()` using delimiter `'-'`. + * - For example, if the `battleId` is `'battle-gen8randombattle-1234567890'`, + * then `split()`ing the string by `'-'` results in `['battle', 'gen8randombattle', '1234567890']`. + * - Pass in the second element (i.e., `'gen8randombattle'` at index `1`) as this value. + * + * @example 'gen8randombattle' + * @warning Fetching will be disabled (regardless of the `disabled` option) if this value is falsy. + * @since 0.1.3 + */ + format?: string; + + /** + * Whether the presets should not be fetched. + * + * @default false + * @since 0.1.3 + */ + disabled?: boolean; +} + +/** + * Returns all the presets for the provided `speciesForme`. + * + * * No need to sanitize the `speciesForme`, as this will find presets of the closest matching `speciesForme` for you. + * - Particularly for randoms, where Pokemon may only have the G-Max forme (with `speciesForme` suffix `'-Gmax'`). + * * Providing `true` to the optional `sort` argument will arrange the presets + * of the current `format` towards the top (i.e., index `0`). + * - Otherwise, they'll be in the order they were added in `presets`. + * * Nomenclature of this function is more closely related to `Model.find()` from `mongoose` than `Array.find()`. + * - `Model.find()` runs a database query and returns an array of matching documents, which this function does do + * (without the database, of course). + * - `Array.find()` only returns a single document, which this function does not do. + * - `Array.filter()`, which is basically what this does, sounded like a terrible name. + * * Returns an empty array (i.e., `[]`) if no matching presets were found. + * + * @since 0.1.3 + */ +export type CalcdexPresetsFinder = ( + speciesForme: string, + sort?: boolean, +) => CalcdexPokemonPreset[]; + +const l = logger('@showdex/pages/Calcdex/usePresets'); + +const sortPresets = ( + presets: CalcdexPokemonPreset[], + genlessFormat: string, +): CalcdexPokemonPreset[] => { + if (!genlessFormat) { + return presets; + } + + // const formatLabel = genlessFormat in FormatLabels ? + // FormatLabels[genlessFormat] : + // // genlessFormat.toUpperCase().slice(0, 3); // e.g., 'huhwtf' -> 'HUH' + // genlessFormat; + + // trailing space prevents something like 'OU-2X' being sorted before 'OU' + // (assuming the genlessFormat is 'ou' and not 'doublesou') + // const formatSearchString = `${formatLabel} `; + + return presets.sort((a, b) => { + // if (a.name.startsWith(formatSearchString)) { + // return -1; + // } + + if (a.format === genlessFormat) { + return -1; + } + + // if (b.name.startsWith(formatSearchString)) { + // return 1; + // } + + if (b.format === genlessFormat) { + return 1; + } + + return 0; + }); +}; + +const UltFormeRegex = /-(?:Mega(?:-X|-Y)?|Gmax)$/; + +/* eslint-disable no-nested-ternary */ + +/** + * Provides convenient tools to access the presets stored in RTK Query. + * + * * Automatically fetches the presets given the `options.format` value is valid. + * - Obviously not the case if `options.disabled` is `true`. + * + * @since 0.1.3 + */ +export const usePresets = ({ + format, + disabled, +}: CalcdexPresetsHookOptions = {}): CalcdexPresetsFinder => { + // note: 'gen8bdspou' requires special treatment since Pokemon like Breloom don't exist in `gen8.json`, + // despite `gen8.json` including presets for the 'bdspou' format (for Gen 8 Pokemon that also exist in BDSP) + // (also rather unfortunately, there's a `gen8bdsprandombattle.json`, so there's that... LOL) + const gen = format ? + format?.includes('bdsp') && !format.includes('random') ? 4 : detectGenFromFormat(format) : + null; // e.g., 8 (if `format` is 'gen8randombattle') + + const baseGen = gen ? `gen${gen}` : null; // e.g., 'gen8' (obviously `gen` shouldn't be 0 here) + const genlessFormat = baseGen ? format.replace(baseGen, '') : null; // e.g., 'randombattle' + const randomsFormat = genlessFormat?.includes('random') ?? false; + + const shouldSkip = disabled || !format || !baseGen || !genlessFormat; + + const { + data: gensPresets, + isLoading: gensLoading, + } = usePokemonGensPresetQuery({ + gen, + format, // if it's BDSP, the query will automatically set the gen to 4 + }, { + skip: shouldSkip || randomsFormat, + }); + + const { + data: randomsPresets, + isLoading: randomsLoading, + } = usePokemonRandomsPresetQuery({ + gen, + format, // if it's BDSP, the query will automatically fetch from `gen8bdsprandombattle.json` + }, { + skip: shouldSkip || !randomsFormat, + }); + + const presets = React.useMemo(() => [ + ...((!randomsFormat && gensPresets) || []), + ...((randomsFormat && randomsPresets) || []), + ].filter(Boolean), [ + gensPresets, + randomsFormat, + randomsPresets, + ]); + + const loading = React.useMemo( + () => gensLoading || randomsLoading, + [gensLoading, randomsLoading], + ); + + const find = React.useCallback(( + speciesForme, + sort, + ) => { + if (!speciesForme) { + if (__DEV__) { + l.warn( + 'Missing required speciesForme argument.', + '\n', 'speciesForme', speciesForme, + // '\n', 'sort', sort || false, + // '\n', 'presets', presets, + '\n', '(You will only see this warning on development.)', + ); + } + + return []; + } + + if (!presets.length || loading) { + // actually, since find() should just be spread alongside the CalcdexPokemon's existing presets (if any), + // this warning would get really annoying + // if (loading && __DEV__) { + // l.warn( + // 'No presets are available since they are currently being fetched.', + // '\n', 'speciesForme', speciesForme, + // '\n', 'sort', sort || false, + // '\n', 'presets', presets, + // '\n', '(You will only see this warning on development.)', + // ); + // } + + return []; + } + + l.debug( + 'Attempting to find presets for', speciesForme, + '\n', 'sort', sort || false, + ); + + // e.g., evals to true w/ speciesForme 'Urshifu-Rapid-Strike-Gmax' or 'Charizard-Mega-X' + const hasUltForme = UltFormeRegex.test(speciesForme); + + // note: ult formes are typically only available in randoms presets + if (hasUltForme && randomsFormat) { + // filter by randoms presets only w/ exact speciesForme match + // (e.g., 'Urshifu' and 'Urshifu-Gmax' both exist in `gen8randombattle.json` [from the pkmn API]) + const ultPresets = presets.filter((p) => p.format.includes(genlessFormat) && p.speciesForme === speciesForme); + + if (ultPresets.length) { + l.debug( + 'Found ultPresets for', speciesForme, + // '\n', 'ultPresets', ultPresets, + ); + + return sort ? sortPresets(ultPresets, genlessFormat) : ultPresets; + } + } + + // since we're still here, that means the ult forme wasn't found in the randoms presets + // e.g., 'Urshifu-Rapid-Strike-Gmax' -> 'Ursifu-Rapid-Strike' + const nonUltForme = hasUltForme ? speciesForme.replace(UltFormeRegex, '') : speciesForme; + + // client sometimes will report a wildcard forme (indicating unrevealed an forme), which can be problematic + // e.g., 'Urshifu-*' -> 'Urshifu' + const nonWildForme = nonUltForme.replace(/-\*$/, ''); + + // try again with the non-ult, non-wildcard forme this time + // (...you know, I have a feeling there's probably a function in one of the @smogon/* or @pkmn/* packages that does all this) + const nonWildPresets = presets.filter((p) => { + // make sure we're only grabbing randoms presets if the format is randoms + // (otherwise, ignore randoms presets for any other format) + const randomsPreset = p.format.includes('random'); + const precondition = randomsFormat ? randomsPreset : !randomsPreset; + + return precondition && p.speciesForme === nonWildForme; + }); + + if (nonWildPresets.length) { + l.debug( + 'Found nonWildPresets for', speciesForme, + '\n', 'nonWildPresets', nonWildPresets, + ); + + return sort ? sortPresets(nonWildPresets, genlessFormat) : nonWildPresets; + } + + // hmm... at this point, we'll try to obtain the actual base species forme + // (unfortunately, there are Pokemon like Ho-Oh, Jangmo-o, Kommo-o, which have dashes in their names, so we gotta account for those) + const hasAltForme = nonWildForme.includes('-') && ![ + 'ho-oh', + 'jangmo-o', + 'indeedee-f', // 'Indeedee-M' should be just 'Indeedee' + 'kommo-o', + 'meowstic-f', // 'Meowstic-M' is just 'Meowstic', hopefully LOL + 'nidoran-m', // verified to be present in `gen7lc.json` + 'nidoran-f', // verified to be present in `gen7lc.json` + 'porygon-z', + ].includes(nonWildForme.toLowerCase()); + + // e.g., 'Aegislash-Shield' -> 'Aegislash', 'Ho-Oh' -> 'Ho-Oh' (left untouched, theoretically) + const baseForme = hasAltForme ? nonWildForme.split('-')[0] : nonWildForme; + + // aiite, well, fuck it lol + const basePresets = presets.filter((p) => { + const randomsPreset = p.format.includes('random'); + const precondition = randomsFormat ? randomsPreset : !randomsPreset; + + // note: we're not doing a hard match here, just a partial one cause we're desparate + // (inb4 "why do I get sets for completely unrelated Pokemon ???") + return precondition && p.speciesForme.includes(baseForme); + }); + + if (__DEV__ && !basePresets.length) { + l.warn( + 'Still couldn\'t find any presets for the initial speciesForme', speciesForme, + '\n', 'Stage 1: nonUltForme', nonUltForme, 'hasUltForme', hasUltForme, + '\n', 'Stage 2: nonWildForme', nonWildForme, 'hasAltForme', hasAltForme, + '\n', 'Stage 3: baseForme', baseForme, 'hasAltForme', hasAltForme, + // '\n', 'presets', presets, + '\n', '(You will only see this warning on development.)', + ); + } + + l.debug( + 'Returning basePresets for', speciesForme, + '\n', 'basePresets', basePresets, + ); + + return basePresets; + }, [ + genlessFormat, + loading, + presets, + randomsFormat, + ]); + + return find; +}; + +/* eslint-enable no-nested-ternary */ From 97634ed8a8a6128cc43623937ca6556d580aa3f2 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Fri, 12 Aug 2022 23:12:33 -0700 Subject: [PATCH 017/143] refac(calcdex): cleaned up useSmogonMatchup --- src/pages/Calcdex/useSmogonMatchup.ts | 31 ++++++++++++++------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/pages/Calcdex/useSmogonMatchup.ts b/src/pages/Calcdex/useSmogonMatchup.ts index d5f25145..6fc8c815 100644 --- a/src/pages/Calcdex/useSmogonMatchup.ts +++ b/src/pages/Calcdex/useSmogonMatchup.ts @@ -1,15 +1,16 @@ import * as React from 'react'; -import type { GenerationNum } from '@pkmn/data'; -import type { - Field as SmogonField, - Move as SmogonMove, - Pokemon as SmogonPokemon, -} from '@smogon/calc'; -import type { CalcdexMatchupResult } from './calcSmogonMatchup'; -import { calcSmogonMatchup } from './calcSmogonMatchup'; +import { calcSmogonMatchup } from '@showdex/utils/calc'; +import type { Generation, MoveName } from '@pkmn/data'; +// import type { +// Field as SmogonField, +// Move as SmogonMove, +// // Pokemon as SmogonPokemon, +// } from '@smogon/calc'; +import type { CalcdexBattleField, CalcdexPokemon } from '@showdex/redux/store'; +import type { CalcdexMatchupResult } from '@showdex/utils/calc'; export type SmogonMatchupHookCalculator = ( - playerMove: SmogonMove, + playerMove: MoveName, ) => CalcdexMatchupResult; /** @@ -20,20 +21,20 @@ export type SmogonMatchupHookCalculator = ( * @since 0.1.2 */ export const useSmogonMatchup = ( - gen: GenerationNum, - playerPokemon: SmogonPokemon, - opponentPokemon: SmogonPokemon, - field?: SmogonField, + dex: Generation, + playerPokemon: CalcdexPokemon, + opponentPokemon: CalcdexPokemon, + field?: CalcdexBattleField, ): SmogonMatchupHookCalculator => React.useCallback(( playerMove, ) => calcSmogonMatchup( - gen, + dex, playerPokemon, opponentPokemon, playerMove, field, ), [ - gen, + dex, playerPokemon, opponentPokemon, field, From d4b9462919b6c5b2605f328190f2905a9afccd81 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Fri, 12 Aug 2022 23:13:45 -0700 Subject: [PATCH 018/143] feat(calcdex): added redux support to calcdex hook --- src/pages/Calcdex/useCalcdex.ts | 525 +++++++------------------------- 1 file changed, 110 insertions(+), 415 deletions(-) diff --git a/src/pages/Calcdex/useCalcdex.ts b/src/pages/Calcdex/useCalcdex.ts index 0db79727..68031ec9 100644 --- a/src/pages/Calcdex/useCalcdex.ts +++ b/src/pages/Calcdex/useCalcdex.ts @@ -1,45 +1,35 @@ import * as React from 'react'; import { Generations } from '@pkmn/data'; import { Dex as PkmnDex } from '@pkmn/dex'; +import { syncBattle } from '@showdex/redux/actions'; +import { calcdexSlice, useCalcdexState, useDispatch } from '@showdex/redux/store'; import { logger } from '@showdex/utils/debug'; -import { - useThunkyBindedActionators, - useThunkyReducer, -} from '@showdex/utils/hooks'; import type { Generation, GenerationNum } from '@pkmn/data'; import type { CalcdexBattleField, + CalcdexBattleState, CalcdexPlayerKey, CalcdexPokemon, - CalcdexReducerInstance, - CalcdexReducerState, -} from './CalcdexReducer'; -import { calcPokemonCalcdexId } from './calcCalcdexId'; -import { CalcdexActionators } from './CalcdexActionators'; -import { CalcdexInitialState, CalcdexReducer } from './CalcdexReducer'; -import { detectPlayerKeyFromBattle } from './detectPlayerKey'; -import { detectPokemonIdent } from './detectPokemonIdent'; -// import { sanitizeSpeciesForme } from './sanitizeSpeciesForme'; -import { syncServerPokemon } from './syncServerPokemon'; -import { usePresetCache } from './usePresetCache'; +} from '@showdex/redux/store'; +// import { usePresetCache } from './usePresetCache'; export interface CalcdexHookProps { battle: Showdown.Battle; - tooltips: Showdown.BattleTooltips; + // tooltips: Showdown.BattleTooltips; // smogon: PkmnSmogon; } export interface CalcdexHookInterface { dex: Generation; - state: CalcdexReducerState; + state: CalcdexBattleState; // dispatch: CalcdexReducerDispatch; - addPokemon: (pokemon: Partial) => Promise; - updatePokemon: (pokemon: Partial) => void; - syncPokemon: (pokemon: Partial) => void; - updateField: (field: Partial) => void; + // addPokemon: (pokemon: Partial) => Promise; + updatePokemon: (playerKey: CalcdexPlayerKey, pokemon: DeepPartial) => void; + // syncPokemon: (pokemon: Partial) => void; + updateField: (field: DeepPartial) => void; setActiveIndex: (playerKey: CalcdexPlayerKey, activeIndex: number) => void; setSelectionIndex: (playerKey: CalcdexPlayerKey, selectionIndex: number) => void; - setAutoSelect: (playerKey: CalcdexPlayerKey, autoSwitch: boolean) => void; + setAutoSelect: (playerKey: CalcdexPlayerKey, autoSelect: boolean) => void; } const l = logger('@showdex/pages/Calcdex/useCalcdex'); @@ -50,16 +40,25 @@ const gens = new Generations(PkmnDex); export const useCalcdex = ({ battle, - tooltips, + // tooltips, // smogon, }: CalcdexHookProps): CalcdexHookInterface => { - const [state, dispatch] = useThunkyReducer( - CalcdexReducer, - { ...CalcdexInitialState }, + // const [state, dispatch] = useThunkyReducer( + // CalcdexReducer, + // { ...CalcdexInitialState }, + // ); + const state = useCalcdexState(); + const dispatch = useDispatch(); + + const battleState = state[battle?.id]; + + l.debug( + '\n', 'state', state, + '\n', 'battleState', battleState, ); - const [prevNonce, setPrevNonce] = React.useState(null); - const presetCache = usePresetCache(); + // const [prevNonce, setPrevNonce] = React.useState(null); + // const presetCache = usePresetCache(); const gen = battle?.gen as GenerationNum; const format = battle?.id?.split?.('-')?.[1]; @@ -69,31 +68,19 @@ export const useCalcdex = ({ const dex = React.useMemo(() => (typeof gen === 'number' && gen > 0 ? { ...pkmnDex, species: (Dex ?? pkmnDex).species, + num: gen, } : null), [ gen, pkmnDex, ]); - const { - addPokemon, - updatePokemon, - updateField, - syncBattleField, - setActiveIndex, - setSelectionIndex, - setAutoSelect, - } = useThunkyBindedActionators( - CalcdexActionators, - dispatch, - ); - // handles `battle` changes React.useEffect(() => { l.debug( 'React.useEffect()', 'received battle update; determining sync changes...', '\n', 'battle.nonce', battle?.nonce, - '\n', 'prevNonce', prevNonce, + '\n', 'battleState.battleNonce', battleState?.battleNonce, '\n', 'battle.p1.pokemon', battle?.p1?.pokemon, '\n', 'battle.p2.pokemon', battle?.p2?.pokemon, '\n', 'battle', battle, @@ -127,374 +114,51 @@ export const useCalcdex = ({ return; } - if (battle.nonce === prevNonce) { + // if (battle.nonce === battleState.battleNonce) { + // l.debug( + // 'React.useEffect()', + // 'ignoring battle update since nonce hasn\'t changed', + // '\n', 'battle.nonce', battle.nonce, + // '\n', 'battleState.battleNonce', battleState.battleNonce, + // '\n', 'battle', battle, + // '\n', 'state', state, + // ); + // + // return; + // } + + if (!battleState?.battleId) { l.debug( 'React.useEffect()', - 'ignoring battle update since nonce hasn\'t changed', - '\n', 'battle.nonce', battle.nonce, - '\n', 'prevNonce', prevNonce, + 'initializing empty battleState', + '\n', 'with battle.nonce', battle.nonce, '\n', 'battle', battle, - '\n', 'state', state, + '\n', 'battleState', battleState, ); - return; - } - - if (battle.nonce !== prevNonce) { + dispatch(calcdexSlice.actions.init({ + battleId: battle.id, + gen: battle.gen, + format, + battleNonce: battle.nonce, + p1: { name: battle.p1?.name, rating: battle.p1?.rating }, + p2: { name: battle.p2?.name, rating: battle.p2?.rating }, + })); + } else if (!battleState?.battleNonce || battle.nonce !== battleState.battleNonce) { l.debug( 'React.useEffect()', - 'updating prevNonce from', prevNonce, - '\n', 'to battle.nonce', battle.nonce, + 'updating battleState via syncBattle()', '\n', 'battle', battle, + '\n', 'battleState', battleState, '\n', 'state', state, ); - setPrevNonce(battle.nonce); + void dispatch(syncBattle({ + battle, + dex, + })); } - // handle battle updates - let battleIdChanged = false; - const battleChanges: Partial = {}; - - (['battleId', 'gen', 'format'] as (keyof CalcdexReducerState)[]).forEach((key) => { - const currentValue = state[key]; - - const value = key === 'format' ? - format : - battle[(key === 'battleId' ? 'id' : key) as 'id' | 'gen']; - - // l.debug('current battleKey', key, 'value', value); - - // if (!value && !['string', 'number', 'boolean'].includes(typeof value)) { - if (value === null || value === undefined) { - l.debug( - 'React.useEffect()', - 'ignoring battle updates for', key, 'due to undefined value', value, - '\n', `state.${key}`, currentValue, - '\n', `battle.${key}`, value, - '\n', 'battle', battle, - ); - - return; - } - - if (currentValue !== value) { - (> battleChanges)[key] = value; - - if (key === 'battleId') { - battleIdChanged = true; - } - } - }); - - /** @todo clear Pokemon in the state -- Pokemon from a previous game still exist in the state, causing all sorts of fuckery */ - if (battleIdChanged || Object.keys(battleChanges).length) { - dispatch({ - type: battleIdChanged ? '@/:init' : '@/:put', - payload: battleChanges, - }); - - if (battleIdChanged && (state.p1.pokemon.length || state.p2.pokemon.length)) { - l.debug( - 'React.useEffect()', - 'battleId has changed; resetting state...', - // '\n', 'ignoring further battle updates to give the re-initialized state time to settle', - '\n', 'battleChanges', battleChanges, - '\n', 'battle', battle, - '\n', 'state', state, - ); - - // return; - } - } - - // handle player updates - void (async () => { - if (!presetCache.available(format)) { - if (presetCache.loading) { - l.debug( - 'React.useEffect() <- presetCache.loading', - '\n', 'presets are already downloading, so fetching will be ignored', - '\n', 'note that this round of rendering will be blocked until fetching is complete', - '\n', 'format', format, - ); - - return; - } - - l.debug( - 'React.useEffect() -> await presetCache.fetch()', - '\n', 'fetching presets from Smogon since none are available', - '\n', 'format', format, - ); - - await presetCache.fetch(format); - } - - const isRandom = format.includes('random'); - - ( ['p1', 'p2']).forEach((playerKey) => { - const player = battle?.[playerKey]; - - // l.debug( - // 'React.useEffect()', - // 'processing player', playerKey, - // '\n', 'player', player, - // '\n', 'battle', battle, - // '\n', 'state', state, - // ); - - if (!player?.sideid) { - l.debug( - 'React.useEffect()', - 'ignoring updates for player', playerKey, 'due to invalid', `battle.${playerKey}`, - '\n', `battle.${playerKey}`, player, - '\n', 'battle', battle, - '\n', 'state', state, - ); - - return; - } - - if (!Array.isArray(player.pokemon) || !player.pokemon.length) { - l.debug( - 'React.useEffect()', - 'ignoring updates for player', playerKey, 'cause of no pokemon lol', - '\n', `battle.${playerKey}.pokemon`, battle?.[playerKey]?.pokemon, - '\n', `battle.${playerKey}`, battle?.[playerKey], - '\n', 'state', state, - ); - - return; - } - - dispatch({ - type: `@${playerKey}/:put`, - payload: { - name: player?.name, - rating: player?.rating, - }, - }); - - // find out which side myPokemon belongs to - const myPokemonSide = detectPlayerKeyFromBattle(battle); - - const isPlayerSide = playerKey === myPokemonSide && - Array.isArray(battle.myPokemon) && - !!battle.myPokemon.length; - - // l.debug( - // 'React.useEffect()', - // '\n', 'playerKey', playerKey, - // '\n', 'myPokemonSide', myPokemonSide, - // '\n', 'isPlayerSide?', isPlayerSide, - // '\n', 'isRandom?', isRandom, - // ); - - const pokemonSource = isPlayerSide && isRandom ? - battle.myPokemon.map((myMon) => { - const ident = detectPokemonIdent( myMon); - const correspondingMon = player.pokemon - .find((pkmn) => detectPokemonIdent(pkmn) === ident); - - // l.debug( - // 'React.useEffect()', - // 'processing myPokemon', ident, 'for playerKey', playerKey, - // '\n', 'myMon', myMon, - // '\n', 'correspondingMon', correspondingMon, - // ); - - if (!correspondingMon) { - return { - ...myMon, - ident, - }; - } - - return { - ...correspondingMon, - ...myMon, - ident, - }; - }) ?? [] : - player.pokemon; - - l.debug( - 'React.useEffect() <- detectPlayerKeyFromBattle()', - '\n', 'playerKey', playerKey, - '\n', 'isPlayerSide?', isPlayerSide, - '\n', 'isRandom?', isRandom, - '\n', 'myPokemonSide', myPokemonSide, - '\n', 'pokemonSource', pokemonSource, - '\n', 'battle', battle, - '\n', 'state', state, - ); - - // update each player's pokemon - const { - autoSelect, - pokemon: pokemonState, - } = state[playerKey]; - - // also find the activeIndex while we're at it - const activeIdent = detectPokemonIdent(player.active?.[0]); - const activeCalcdexId = calcPokemonCalcdexId({ - ...player?.active?.[0], - ident: activeIdent, - }); - - pokemonSource.forEach((mon, i) => void (async () => { - // unfortunately, there's no true static ID for each Pokemon, so we have to do this weird workaround. - // since the calcdexId hash depends the `ident` in the Pokemon object, it's also pretty unreliable. - // detectPokemonIdent() attempts to rebuild a static `ident`, with some edge cases where it fails. - // why? sometimes `ident` will be falsy from the battle state; - // other times, it'll just be flat out wrong cause it doesn't include the Pokemon's specific form. - // (e.g., for 'p1: Landorus-Therian', `ident` will sometimes just say 'p1: Landorus', which causes the extension - // to spawn Satan himself... err, I mean, horribly, horribly break.) - // not to mention when people nickname their Pokemon... the server replaces the speciesForme in the `ident`. - // (e.g., if I name my Landorus-Therian 'Wacko', `ident` will read 'p1: Wacko', so imagine when the extension - // tries to find a 'p1: Landorus' that was sent from the server... hmm... yikes. [i.e., nothing updates!]) - // (...don't even think about using `searchid`; it's just as falsy as `ident`... sadge) - const ident = detectPokemonIdent(mon); - const calcdexId = calcPokemonCalcdexId({ ...mon, ident }); - - if (!calcdexId) { - l.debug( - 'React.useEffect()', - 'ignoring updates for pokemon of player', playerKey, 'due to invalid calcdexId', calcdexId, - '\n', 'ident', ident, - '\n', 'mon', mon, - '\n', `battle.${playerKey}.pokemon`, player.pokemon, - ); - - return; - } - - // const index = pokemonState.findIndex((p) => detectPokemonIdent(p) === ident); - const index = pokemonState - .findIndex((p) => (p?.calcdexId ?? calcPokemonCalcdexId({ ...p, ident: detectPokemonIdent(p) })) === calcdexId); - - if (index < 0 && pokemonState.length >= 6) { - l.warn( - 'React.useEffect() <- pokemonState.findIndex()', - '\n', 'could not find Pokemon with calcdexId', calcdexId, 'at current index', i, - '\n', 'index', index, - // '\n', 'pokemonState[', index, ']', pokemonState[index], - '\n', 'pokemonState', pokemonState, - // '\n', 'pokemonState idents', pokemonState.map((p) => detectPokemonIdent(p)), - '\n', 'pokemonState calcdexIds', pokemonState.map((p) => ({ - ident: p?.ident, - calcdexId: p?.calcdexId ?? calcPokemonCalcdexId(p), - })), - '\n', 'ident', ident, - '\n', 'mon', mon, - '\n', `battle.${playerKey}.pokemon`, player.pokemon, - ); - - return; - } - - // if the current player is `p1`, check for a corresponding `myPokemon`, if available - const serverPokemon = myPokemonSide && playerKey === myPokemonSide ? - battle.myPokemon?.find((p) => { - const pIdent = detectPokemonIdent( p)?.replace?.(/-Mega/gi, ''); - // const pCalcdexId = calcPokemonCalcdexId( p); - const didMatch = pIdent === ident; - // const didMatch = pCalcdexId === calcdexId; - - // l.debug( - // 'serverPokemon', - // '\n', 'pIdent', pIdent, 'ident', ident, '?', didMatch, - // '\n', 'myPokemonSide', myPokemonSide, 'playerKey', playerKey, - // ); - - return didMatch; - }) : - null; - - // l.debug( - // 'myPokemonSide', myPokemonSide, 'playerKey', playerKey, - // '\n', 'serverPokemon', serverPokemon, - // ); - - // const newPokemon: CalcdexPokemon = { - // ...mon, - // ...( serverPokemon), - // }; - - let newPokemon: CalcdexPokemon = { - ...( mon), - }; - - if (serverPokemon || isRandom) { - newPokemon = syncServerPokemon(dex, presetCache, format, newPokemon, serverPokemon); - } - - // found the pokemon, so update it - if (index > -1) { - l.debug( - 'React.useEffect() -> updatePokemon()', - '\n', 'syncing Pokemon', calcdexId, `(${mon?.ident})`, 'with mutation', newPokemon, - '\n', `state.${playerKey}.pokemon[`, index, ']', pokemonState[index], - '\n', 'i', i, 'index', index, - ); - - if (activeCalcdexId === calcdexId) { - setActiveIndex(playerKey, index); - - if (autoSelect) { - setSelectionIndex(playerKey, index); - } - } - - // update the mon at the current index - // (`:put` is used when serverPokemon is available, otherwise `:sync`) - updatePokemon(tooltips, newPokemon, !serverPokemon); - - return; - } - - l.debug( - 'React.useEffect() -> await addPokemon()', - '\n', 'adding pokemon', newPokemon.ident, 'to index', pokemonState.length, - '\n', 'newPokemon', newPokemon, - '\n', `state.${playerKey}.pokemon`, pokemonState, - '\n', 'playerKey', playerKey, - '\n', 'i', i, 'index', index, - ); - - // add the mon to whatever index lol - try { - await addPokemon(dex, tooltips, presetCache, newPokemon, format); - } catch (error) { - l.error( - 'React.useEffect() <- await addPokemon()', - '\n', error, - '\n', 'playerKey', playerKey, - '\n', 'i', i, 'index', index, - '\n', 'newPokemon', newPokemon, - ); - } - - // l.debug( - // 'React.useEffect() <- await addPokemon()', - // '\n', 'playerKey', playerKey, - // '\n', 'i', i, 'index', index, - // '\n', 'newPokemon', newPokemon, - // ); - })()); - }); - })(); - - // l.debug( - // 'React.useEffect() -> syncBattleField()', - // '\n', 'battle', battle, - // '\n', 'state', state, - // ); - - // handle field changes - syncBattleField(battle); - l.debug( 'React.useEffect()', 'completed battle state sync for nonce', battle.nonce, @@ -502,33 +166,64 @@ export const useCalcdex = ({ '\n', 'state', state, ); }, [ - addPokemon, battle, battle?.nonce, + battleState, dex, dispatch, format, - presetCache, - prevNonce, - setActiveIndex, - setSelectionIndex, - // smogon, + // presetCache, state, - syncBattleField, - tooltips, - updatePokemon, ]); return { dex, - state, - // dispatch, - addPokemon: (pokemon) => addPokemon(dex, tooltips, presetCache, pokemon, format), - updatePokemon: (pokemon) => updatePokemon(tooltips, pokemon), - syncPokemon: (pokemon) => updatePokemon(tooltips, pokemon, true), - updateField, - setActiveIndex, - setSelectionIndex, - setAutoSelect, + + state: battleState || { + battleId: null, + gen: null, + format: null, + field: null, + p1: { + name: null, + rating: null, + activeIndex: -1, + selectionIndex: 0, + pokemon: [], + }, + p2: { + name: null, + rating: null, + activeIndex: -1, + selectionIndex: 0, + pokemon: [], + }, + }, + + updatePokemon: (playerKey, pokemon) => dispatch(calcdexSlice.actions.updatePokemon({ + battleId: battle?.id, + playerKey, + pokemon, + })), + + updateField: (field) => dispatch(calcdexSlice.actions.updateField({ + battleId: battle?.id, + field, + })), + + setActiveIndex: (playerKey, activeIndex) => dispatch(calcdexSlice.actions.updatePlayer({ + battleId: battle?.id, + [playerKey]: { activeIndex }, + })), + + setSelectionIndex: (playerKey, selectionIndex) => dispatch(calcdexSlice.actions.updatePlayer({ + battleId: battle?.id, + [playerKey]: { selectionIndex }, + })), + + setAutoSelect: (playerKey, autoSelect) => dispatch(calcdexSlice.actions.updatePlayer({ + battleId: battle?.id, + [playerKey]: { autoSelect }, + })), }; }; From 703f83c3f4a5184509433cc05fc8a1881dccd5e7 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Fri, 12 Aug 2022 23:14:41 -0700 Subject: [PATCH 019/143] chore(calcdex): deeshed deprecated files --- src/pages/Calcdex/CalcdexActionators.ts | 465 --------- src/pages/Calcdex/CalcdexReducer.ts | 1046 --------------------- src/pages/Calcdex/fetchPokemonMovesets.ts | 87 -- src/pages/Calcdex/fetchPokemonPresets.ts | 124 --- src/pages/Calcdex/sanitizeSpeciesForme.ts | 25 - src/pages/Calcdex/syncServerPokemon.ts | 185 ---- src/pages/Calcdex/usePresetCache.ts | 430 --------- 7 files changed, 2362 deletions(-) delete mode 100644 src/pages/Calcdex/CalcdexActionators.ts delete mode 100644 src/pages/Calcdex/CalcdexReducer.ts delete mode 100644 src/pages/Calcdex/fetchPokemonMovesets.ts delete mode 100644 src/pages/Calcdex/fetchPokemonPresets.ts delete mode 100644 src/pages/Calcdex/sanitizeSpeciesForme.ts delete mode 100644 src/pages/Calcdex/syncServerPokemon.ts delete mode 100644 src/pages/Calcdex/usePresetCache.ts diff --git a/src/pages/Calcdex/CalcdexActionators.ts b/src/pages/Calcdex/CalcdexActionators.ts deleted file mode 100644 index f6409e71..00000000 --- a/src/pages/Calcdex/CalcdexActionators.ts +++ /dev/null @@ -1,465 +0,0 @@ -import { PokemonNatures } from '@showdex/consts'; -import { logger } from '@showdex/utils/debug'; -import type { AbilityName, Generation as PkmnGeneration } from '@pkmn/data'; -import type { - ThunkyReducerAction, - ThunkyReducerActionator, - ThunkyReducerActionatorMap, -} from '@showdex/utils/hooks'; -import type { - CalcdexBattleField, - CalcdexPlayerKey, - CalcdexPokemon, - CalcdexReducerInstance, -} from './CalcdexReducer'; -import type { PresetCacheHookInterface } from './usePresetCache'; -import { calcPokemonCalcdexId } from './calcCalcdexId'; -import { calcPokemonCalcdexNonce } from './calcCalcdexNonce'; -// import { calcPokemonStats } from './calcPokemonStats'; -import { detectPlayerKeyFromPokemon } from './detectPlayerKey'; -import { detectPokemonIdent } from './detectPokemonIdent'; -import { detectSpeciesForme } from './detectSpeciesForme'; -import { detectToggledAbility } from './detectToggledAbility'; -import { fetchPokemonMovesets } from './fetchPokemonMovesets'; -import { fetchPokemonPresets } from './fetchPokemonPresets'; -import { sanitizePokemon } from './sanitizePokemon'; -import { syncField } from './syncField'; - -export type CalcdexReducerAction< - V extends void | Promise = void, -> = ThunkyReducerAction; - -export type CalcdexActionator< - A extends unknown[], - V extends void | Promise = void, -> = ThunkyReducerActionator CalcdexReducerAction>; - -export interface CalcdexActionatorMap extends ThunkyReducerActionatorMap { - addPokemon: CalcdexActionator<[ - dex: PkmnGeneration, - tooltips: Showdown.BattleTooltips, - cache: PresetCacheHookInterface, - pokemon: Partial, - format?: string, - ], Promise>; - - updatePokemon: CalcdexActionator<[ - tooltips: Showdown.BattleTooltips, - pokemon: Partial, - shouldSync?: boolean, - ]>; - - updateField: CalcdexActionator<[ - field: Partial, - ]>; - - syncBattleField: CalcdexActionator<[ - battle: Showdown.Battle, - ]>; - - setActiveIndex: CalcdexActionator<[ - playerKey: CalcdexPlayerKey, - activeIndex: number, - ]>; - - setSelectionIndex: CalcdexActionator<[ - playerKey: CalcdexPlayerKey, - selectionIndex: number, - ]>; - - setAutoSelect: CalcdexActionator<[ - playerKey: CalcdexPlayerKey, - autoSelect: boolean, - ]>; -} - -const l = logger('@showdex/pages/Calcdex/CalcdexActionators'); - -export const addPokemon: CalcdexActionatorMap['addPokemon'] = ( - dex, - tooltips, - cache, - pokemon, - format, -) => async (dispatch, getState) => { - const ident = detectPokemonIdent(pokemon); - const speciesForme = detectSpeciesForme(pokemon); - - if (!ident || !speciesForme) { - l.warn( - 'addPokemon() <- detectPokemonIdent(), detectSpeciesForme()', - '\n', 'could not detect ident/speciesForme from Pokemon with ident', ident, - '\n', 'speciesForme', speciesForme, - '\n', 'pokemon', pokemon, - '\n', 'format', format, - ); - - return; - } - - const playerKey = detectPlayerKeyFromPokemon(pokemon); - - if (!playerKey) { - l.warn( - 'addPokemon() <- detectPlayerKeyFromPokemon()', - '\n', 'could not detect playerKey from Pokemon with ident', ident, - '\n', 'playerKey', playerKey, - '\n', 'speciesForme', speciesForme, - '\n', 'pokemon', pokemon, - '\n', 'format', format, - ); - - return; - } - - const state = getState(); - - const existingPokemon = state[playerKey].pokemon - ?.find((p) => detectPokemonIdent(p) === ident); - - if (existingPokemon) { - l.warn( - 'addPokemon()', - '\n', 'Pokemon with ident', ident, 'already exists for player', playerKey, - '\n', 'existingPokemon', existingPokemon, - '\n', 'speciesForme', speciesForme, - '\n', 'pokemon', pokemon, - '\n', 'format', format, - ); - - return; - } - - const newPokemon = sanitizePokemon(pokemon); - - if (typeof dex?.species?.get === 'function') { - const species = dex.species.get(pokemon.speciesForme); - - if (Object.keys(species?.abilities || {}).length) { - newPokemon.abilities = Object.values(species.abilities); - - if (newPokemon.abilities?.[0] && !newPokemon.ability) { - [newPokemon.ability] = newPokemon.abilities; - } - - // l.debug( - // 'addPokemon() <- dex.species.get().abilities', - // '\n', 'newPokemon.ability', newPokemon.ability, - // '\n', 'newPokemon.abilities', newPokemon.abilities, - // '\n', 'speciesForme', pokemon.speciesForme, - // '\n', 'ident', ident, - // '\n', 'newPokemon', newPokemon, - // ); - } - - if (species?.baseStats) { - newPokemon.baseStats = { - ...species.baseStats, - }; - - // l.debug( - // 'addPokemon() <- dex.species.get().baseStats', - // '\n', 'newPokemon.baseStats', newPokemon.baseStats, - // '\n', 'speciesForme', pokemon.speciesForme, - // '\n', 'ident', ident, - // '\n', 'newPokemon', newPokemon, - // ); - } - } - - if (!newPokemon.nature) { - [newPokemon.nature] = PokemonNatures; - } - - // l.debug( - // 'addPokemon() <- @showdex/consts/PokemonNatures', - // '\n', 'newPokemon.nature', newPokemon.nature, - // '\n', 'newPokemon', newPokemon, - // ); - - if (typeof tooltips?.getPokemonTypes === 'function') { - newPokemon.types = tooltips.getPokemonTypes( newPokemon); - - // l.debug( - // 'addPokemon() <- tooltips.getPokemonTypes()', - // '\n', 'types', newPokemon.types, - // '\n', 'pokemon', newPokemon, - // '\n', 'ident', ident, - // '\n', 'speciesForme', speciesForme, - // '\n', 'format', format, - // ); - } - - // l.debug( - // 'addPokemon() -> await fetchPokemonMovesets()', - // '\n', 'newPokemon', 'newPokemon', - // '\n', 'ident', ident, - // '\n', 'speciesForme', speciesForme, - // '\n', 'format', format, - // ); - - // grab the Pokemon's movesets - const movesetPokemon = await fetchPokemonMovesets(dex, newPokemon, format); - - // l.debug( - // 'addPokemon() <- await fetchPokemonMovesets()', - // '\n', 'movesetPokemon', movesetPokemon, - // '\n', 'ident', ident, - // '\n', 'speciesForme', speciesForme, - // '\n', 'format', format, - // ); - - if (Array.isArray(movesetPokemon?.moveState?.learnset)) { - newPokemon.moveState = movesetPokemon.moveState; - - // l.debug( - // 'setting newPokemon.moveState to', movesetPokemon.moveState, - // '\n', 'newPokemon', newPokemon, - // '\n', 'ident', ident, - // '\n', 'speciesForme', speciesForme, - // '\n', 'format', format, - // ); - } - - // grab the Pokemon's learnsets and Smogon presets (if available) - // l.debug( - // 'addPokemon() -> await fetchPokemonPresets()', - // '\n', 'newPokemon', newPokemon, - // '\n', 'format', format, - // '\n', 'ident', ident, - // ); - - /** - * @note If you notice that `fetchPokemonPresets()` doesn't return from its `Promise`, - * you may be hard-pressed to find that all you had to do was reload the extension in Chrome's settings LOL. - * - * This is due to the `runtimeFetch()` util that was passed into initializing `Smogon` in `Calcdex.bootstrap.ts`. - * `runtimeFetch()` relies on a background service worker (handled in `background.ts`) to `fetch()` the data - * (to get around Chrome's strict enforcement of CORS). - * - * Theoretically, this should only be an issue in development... *should*. - */ - const presetPokemon = await fetchPokemonPresets(dex, cache, newPokemon, format); - - if (Object.keys(presetPokemon).length) { - Object.entries(presetPokemon).forEach(([key, value]) => { - newPokemon[key] = value; - }); - } - - newPokemon.abilityToggled = detectToggledAbility(newPokemon); - - // if the Pokemon's revealed ability and/or item match their dirty counterparts (lol), - // clear the dirty values - if (newPokemon.ability === newPokemon.dirtyAbility) { - newPokemon.dirtyAbility = null; - } - - // prevItem here accounts for knocked-off or consumed items - // (in which case `item` would be falsy, falling back to prevItem) - if ((newPokemon.item || newPokemon.prevItem) === newPokemon.dirtyItem) { - newPokemon.dirtyItem = null; - } - - // l.debug( - // 'addPokemon() <- await fetchPokemonPresets()', - // '\n', 'presetPokemon', presetPokemon, - // '\n', 'newPokemon', newPokemon, - // '\n', 'ident', ident, - // '\n', 'format', format, - // ); - - const calcdexId = calcPokemonCalcdexId(newPokemon); - - if (!newPokemon?.calcdexId || newPokemon.calcdexId !== calcdexId) { - newPokemon.calcdexId = calcdexId; - } - - newPokemon.calcdexNonce = calcPokemonCalcdexNonce(newPokemon); - - // l.debug( - // 'addPokemon() -> dispatch()', - // '\n', 'type', `@${playerKey}/pokemon:post`, - // '\n', 'payload', newPokemon, - // '\n', 'ident', ident, - // ); - - dispatch({ - type: `@${playerKey}/pokemon:post`, - payload: newPokemon, - }); -}; - -export const updatePokemon: CalcdexActionatorMap['updatePokemon'] = ( - tooltips, - pokemon, - shouldSync, -) => (dispatch) => { - // const ident = detectPokemonIdent(pokemon); - const playerKey = detectPlayerKeyFromPokemon(pokemon); - - if (!playerKey) { - l.warn( - 'updatePokemon()', - '\n', 'could not detect playerKey from pokemon', pokemon, - '\n', 'playerKey', playerKey, - ); - - return; - } - - const updatedPokemon = > { ...pokemon }; - - if (typeof tooltips?.getPokemonTypes === 'function') { - const types = tooltips.getPokemonTypes( updatedPokemon); - - if (types?.length && types[0] !== '???' && JSON.stringify(updatedPokemon.types) !== JSON.stringify(types)) { // kekw - updatedPokemon.types = types; - - // l.debug( - // 'updatePokemon() <- tooltips.getPokemonTypes()', - // '\n', 'types', types, - // '\n', 'updatedPokemon.types', updatedPokemon.types, - // '\n', 'updatedPokemon', updatedPokemon, - // '\n', 'ident', ident, - // '\n', 'playerKey', playerKey, - // ); - } - } - - // l.debug( - // 'updatePokemon() -> dispatch()', - // '\n', 'type', `@${playerKey}/pokemon:${shouldSync ? 'sync' : 'put'}`, - // '\n', 'payload', updatedPokemon, - // '\n', 'ident', ident, - // '\n', 'playerKey', playerKey, - // ); - - dispatch({ - type: `@${playerKey}/pokemon:${shouldSync ? 'sync' : 'put'}`, - payload: updatedPokemon, - }); -}; - -export const updateField: CalcdexActionatorMap['updateField'] = ( - field, -) => (dispatch) => { - // l.debug( - // 'updateField() -> dispatch()', - // '\n', 'type', '@field/:put', - // '\n', 'payload', field, - // ); - - dispatch({ - type: '@field/:put', - payload: field, - }); -}; - -export const syncBattleField: CalcdexActionatorMap['syncBattleField'] = ( - battle, -) => (dispatch, getState) => { - const state = getState(); - - const { activeIndex: attackerIndex } = state.p1; - const { activeIndex: defenderIndex } = state.p2; - - // l.debug( - // 'syncBattleField() -> syncField()', - // '\n', 'state.field', state.field, - // '\n', 'attackerIndex', attackerIndex, - // '\n', 'defenderIndex', defenderIndex, - // '\n', 'battle', battle, - // '\n', 'state', state, - // ); - - const syncedField = syncField( - state.field, - battle, - attackerIndex, - defenderIndex, - ); - - if (!syncedField?.gameType) { - l.debug( - 'syncBattleField() <- syncField()', - '\n', 'ignoring field updates due to invalid synced field', syncedField, - '\n', 'state.field', state.field, - '\n', 'attackerIndex', attackerIndex, - '\n', 'defenderIndex', defenderIndex, - '\n', 'battle', battle, - '\n', 'state', state, - ); - - return; - } - - // l.debug( - // 'syncBattleField() -> dispatch()', - // '\n', 'type', '@field/:put', - // '\n', 'payload', syncedField, - // ); - - dispatch({ - type: '@field/:put', - payload: syncedField, - }); -}; - -export const setActiveIndex: CalcdexActionatorMap['setActiveIndex'] = ( - playerKey, - activeIndex, -) => (dispatch) => { - // l.debug( - // 'setActiveIndex() -> dispatch()', - // '\n', 'type', `@${playerKey}/activeIndex:put`, - // '\n', 'payload', activeIndex, - // ); - - dispatch({ - type: `@${playerKey}/activeIndex:put`, - payload: activeIndex, - }); -}; - -export const setSelectionIndex: CalcdexActionatorMap['setSelectionIndex'] = ( - playerKey, - selectionIndex, -) => (dispatch) => { - // l.debug( - // 'setSelectionIndex() -> dispatch()', - // '\n', 'type', `@${playerKey}/selectionIndex:put`, - // '\n', 'payload', selectionIndex, - // ); - - dispatch({ - type: `@${playerKey}/selectionIndex:put`, - payload: selectionIndex, - }); -}; - -export const setAutoSelect: CalcdexActionatorMap['setAutoSelect'] = ( - playerKey, - autoSelect, -) => (dispatch) => { - // l.debug( - // 'setAutoSelect() -> dispatch()', - // '\n', 'type', `@${playerKey}/autoSelect:put`, - // '\n', 'payload', autoSelect, - // ); - - dispatch({ - type: `@${playerKey}/autoSelect:put`, - payload: autoSelect, - }); -}; - -export const CalcdexActionators: CalcdexActionatorMap = { - addPokemon, - updatePokemon, - updateField, - syncBattleField, - setActiveIndex, - setSelectionIndex, - setAutoSelect, -}; diff --git a/src/pages/Calcdex/CalcdexReducer.ts b/src/pages/Calcdex/CalcdexReducer.ts deleted file mode 100644 index 95cf848a..00000000 --- a/src/pages/Calcdex/CalcdexReducer.ts +++ /dev/null @@ -1,1046 +0,0 @@ -import * as React from 'react'; -import { logger } from '@showdex/utils/debug'; -import type { - AbilityName, - GenerationNum, - ItemName, - MoveName, -} from '@pkmn/data'; -import type { State as SmogonState } from '@smogon/calc'; -import type { ThunkyReducerDispatch } from '@showdex/utils/hooks'; -import { calcPokemonCalcdexId } from './calcCalcdexId'; -import { calcPokemonCalcdexNonce } from './calcCalcdexNonce'; -import { detectPokemonIdent } from './detectPokemonIdent'; -import { detectToggledAbility } from './detectToggledAbility'; -import { sanitizeField } from './sanitizeField'; -import { sanitizePlayerSide } from './sanitizePlayerSide'; -import { sanitizePokemon } from './sanitizePokemon'; -import { syncPokemon } from './syncPokemon'; - -export interface CalcdexMoveState { - /** - * Should only consist of moves that were revealed during the battle. - * - * * These moves should have the highest render priority - * (i.e., should be at the top of the list). - * * This is usually accessible within the client `Showdown.Pokemon` object, - * under the `moveTrack` property. - * - * @default [] - * @since 0.1.0 - */ - revealed: (MoveName | string)[]; - - /** - * Should only consist of moves that the Pokemon can legally learn. - * - * * These moves should be rendered after those in `revealed`. - * * Moves that exist in `revealed` should be filtered out. - * - * @default [] - * @since 0.1.0 - */ - learnset: (MoveName | string)[]; - - /** - * Optional moves, including potentially illegal ones for formats like `gen8anythinggoes` (I think lmao). - * - * * These moves, if specified, should be rendered last. - * * Moves that exist in `revealed` and `learnsets` should be filtered out. - * - * @default [] - * @since 0.1.0 - */ - other: (MoveName | string)[]; -} - -/** - * Pokemon set, ~~basically~~ probably. - * - * * Types for some properties are more specifically typed, - * such as defining `item` as type `ItemName` instead of `string` (from generic `T` in `PokemonSet`). - * - * Any mention of the word *preset* here (and anywhere else within the code) is meant to be used interchangably with *set*. - * * Avoids potential confusion (and hurting yourself in that confusion) by avoiding the JavaScript keyword `set`. - * * Similar to naming a property *delete*, you can imagine having to destructure `delete` as a variable! - * * Also, `setSet()` or `setPreset()`? Hmm... - * - * @since 0.1.0 - */ -export interface CalcdexPokemonPreset { - /** - * Name of the preset. - * - * * Unfortunately, when accessing the presets via `smogon.sets()` in `@pkmn/smogon` without a `format` argument, - * none of the presets have their tier prefixed to the name. - * - e.g., "OU Choice Band" (how Smogon does it) vs. "Choice Band" (what `smogon.sets()` returns) - * * This makes it difficult to differentiate presets between each tier, - * especially if you're using the `name` as the value. - * - e.g., "OU Choice Band" and "UU Choice Band" will both have the `name` "Choice Band". - * * For indexing, it's better to use the `calcdexId` property, - * which is calculated from the actual preset values themselves. - * - For instance, "OU Choice Band" may run a different item/nature/moveset than "UU Choice Band", - * resulting in a different `calcdexId`. - * - * @example 'Choice Band' - * @since 0.1.0 - */ - name?: string; - - /** - * Unique ID (via `uuid`) generated from a serialized checksum of this preset. - * - * * For more information about why this property exists, - * see the `name` property. - * * Note that a preset won't have a `calcdexNonce` property since none of the preset's - * properties should be mutable (they're pre-*set*, after all!). - * - * @since 0.1.0 - */ - calcdexId?: string; - - format?: string; - species?: string; - level?: number; - gender?: Showdown.GenderName; - shiny?: boolean; - ability?: AbilityName; - altAbilities?: AbilityName[]; - item?: ItemName; - altItems?: ItemName[]; - moves?: MoveName[]; - altMoves?: MoveName[]; - nature?: Showdown.PokemonNature; - ivs?: Showdown.StatsTable; - evs?: Showdown.StatsTable; - happiness?: number; - pokeball?: string; - hpType?: string; - gigantamax?: boolean; -} - -/* eslint-disable @typescript-eslint/indent */ - -/** - * Lean version of the `Showdown.Pokemon` object used by the official client. - * - * * Basically `Showdown.Pokemon` without the class functions like `isGrounded()`. - * - * @since 0.1.0 - */ -export type CalcdexLeanPokemon = Omit>, - | 'ability' - | 'baseAbility' - | 'item' - | 'hpcolor' - | 'moves' - | 'moveTrack' - | 'nature' - | 'prevItem' - | 'side' - | 'sprite' ->; - -/* eslint-enable @typescript-eslint/indent */ - -export interface CalcdexPokemon extends CalcdexLeanPokemon { - /** - * Internal unqiue ID used by the extension. - * - * @since 0.1.0 - */ - calcdexId?: string; - - /** - * Internal checksum of the Pokemon's mutable properties used by the extension. - * - * @since 0.1.0 - */ - calcdexNonce?: string; - - /** - * Whether the Pokemon object originates from the client or server. - * - * * If the type if `Showdown.Pokemon`, then the Pokemon is *probably* from the client. - * * If the type is `Showdown.ServerPokemon`, then the Pokemon is from the server (duh). - * * Used to determine which fields to overwrite when syncing. - * - * @default false - * @since 0.1.0 - */ - serverSourced?: boolean; - - /** - * Unsanitized version of `speciesForme`, primarily used for determining Z/Max/G-Max moves. - * - * @since 0.1.2 - */ - rawSpeciesForme?: string; - - /** - * Current types of the Pokemon. - * - * * Could change depending on the Pokemon's ability, like *Protean*. - * * Should be set via `tooltips.getPokemonTypes()`. - * - * @since 0.1.0 - */ - types?: readonly Showdown.TypeName[]; - - /** - * Ability of the Pokemon. - * - * @since 0.1.0 - */ - ability?: AbilityName; - - /** - * Ability of the Pokemon, but it's filthy af. - * - * * Stank. - * - * @since 0.1.0 - */ - dirtyAbility?: AbilityName; - - /** - * Base ability of the Pokemon. - * - * @since 0.1.0 - */ - baseAbility?: AbilityName; - - /** - * Some abilities are conditionally toggled, such as *Flash Fire*. - * - * * While we don't have to worry about those conditions, - * we need to keep track of whether the ability is active. - * * If the ability is not in `PokemonToggleAbilities` in `consts`, - * this value will always be `true`, despite the default value being `false`. - * - * @see `PokemonToggleAbilities` in `src/consts/abilities.ts`. - * @default false - * @since 0.1.2 - */ - abilityToggled?: boolean; - - /** - * Possible abilities of the Pokemon. - * - * @default [] - * @since 0.1.0 - */ - abilities?: AbilityName[]; - - /** - * Alternative abilities from the currently applied `preset`. - * - * @since 0.1.0 - */ - altAbilities?: AbilityName[]; - - /** - * Nature of the Pokemon. - * - * @since 0.1.0 - */ - nature?: Showdown.PokemonNature; - - /** - * Possible natures of the Pokemon. - * - * @deprecated Use `PokemonNatures` from `@showdex/consts` instead. - * @default [] - * @since 0.1.0 - */ - natures?: Showdown.PokemonNature[]; - - /** - * Item being held by the Pokemon. - * - * * Unlike `dirtyItem`, any falsy value (i.e., `''`, `null`, or `undefined`) is considered to be *no item*. - * * This (and `prevItem`) is redefined with the `ItemName` type to make `@pkmn/*` happy. - * - * @since 0.1.0 - */ - item?: ItemName; - - /** - * Alternative items from the currently applied `preset`. - * - * @since 0.1.0 - */ - altItems?: ItemName[]; - - /** - * Keeps track of the user-modified item as to not modify the actual `item` (or lack thereof) synced from the `battle` state. - * - * * Since it's possible for a Pokemon to have no item, an empty string (i.e., `''`) will indicate that the Pokemon intentionally has no item. - * - You will need to cast the empty string to type `ItemName` (e.g., ` ''`) since it doesn't exist on that type. - * - Currently not unioning an empty string with `ItemName` since TypeScript will freak the fucc out. - * * Any other falsy value (i.e., `null` or `undefined`) will fallback to the Pokemon's `item` (or lack thereof, if it got *Knocked Off*, for instance). - * - Under-the-hood, the *Nullish Coalescing Operator* (i.e., `??`) is being used, which falls back to the right-hand value if the left-hand value is `null` or `undefined`. - * - * @default null - * @since 0.1.0 - */ - dirtyItem?: ItemName; - - /** - * Previous item that was held by the Pokemon. - * - * @since 0.1.0 - */ - prevItem?: ItemName; - - /** - * Individual Values (IVs) of the Pokemon. - * - * @since 0.1.0 - */ - ivs?: Showdown.PokemonSet['ivs']; - - /** - * Effort Values (EVs) of the Pokemon. - * - * @since 0.1.0 - */ - evs?: Showdown.PokemonSet['evs']; - - /** - * Moves currently assigned to the Pokemon. - * - * * Typically contains moves set via user input or Smogon sets. - * * Should not be synced with the current `app.curRoom.battle` state. - * - Unless the originating Pokemon object is a `Showdown.ServerPokemon`. - * - In that instance, `serverSourced` should be `true`. - * - * @since 0.1.0 - */ - moves?: MoveName[]; - - /** - * Alternative moves from the currently applied `preset`. - * - * * Should be rendered within the moves dropdown, similar to the moves in the properties of `moveState`. - * * For instance, there may be more than 4 moves from a random preset. - * - The first 4 moves are set to `moves`. - * - All possible moves from the preset (including the 4 that were set to `moves`) are set to this property. - * - * @since 0.1.0 - */ - altMoves?: MoveName[]; - - /** - * Whether the Pokemon is using Z/Max/G-Max moves. - * - * * Using the term *ultimate* (thanks Blizzard/Riot lmaoo) to cover the nomenclature for both Z (gen 7) and Max/G-Max (gen 8) moves. - * - * @since 0.1.2 - */ - useUltimateMoves?: boolean; - - /** - * Moves revealed by the Pokemon to the opponent. - * - * @since 0.1.0 - */ - moveTrack?: [moveName: MoveName, ppUsed: number][]; - - /** - * Categorized moves of the Pokemon. - * - * @since 0.1.0 - */ - moveState?: CalcdexMoveState; - - /** - * Keeps track of user-modified boosts as to not modify the actual boosts from the `battle` state. - * - * @default {} - * @since 0.1.0 - */ - dirtyBoosts?: Partial>; - - /** - * Base stats of the Pokemon based on its species. - * - * @default { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0 } - * @since 0.1.0 - */ - baseStats?: Partial; - - /** - * Calculated stats of the Pokemon based on its current properties. - * - * @default { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0 } - * @since 0.1.0 - */ - calculatedStats?: Partial; - - /** - * Whether to calculate move damages as critical hits. - * - * @default false - * @since 0.1.0 - */ - criticalHit?: boolean; - - /** - * Remaining number of turns the Pokemon is poisoned for. - * - * * This property is only used by `calculate()` in `@smogon/calc`. - * * Value of `0` means the Pokemon is not poisoned. - * - * @default 0 - * @since 0.1.0 - */ - toxicCounter?: number; - - /** - * Preset that's currently being applied to the Pokemon. - * - * * Could use the preset's `name`, but you may run into some issues with uniqueness. - * - See the `name` property in `CalcdexPokemonPreset` for more information. - * * Recommended you use the preset's `calcdexId` as this property's value instead. - * - * @since 0.1.0 - */ - preset?: string; - - /** - * Available presets (i.e., sets) for the Pokemon. - * - * @todo change this to `string[]` (of calcdexId's) for better memory management - * @default [] - * @since 0.1.0 - */ - presets?: CalcdexPokemonPreset[]; - - /** - * Whether the preset should automatically update based on revealed moves (i.e., `moveState.revealed`). - * - * @default true - * @since 0.1.0 - */ - autoPreset?: boolean; -} - -/* eslint-disable @typescript-eslint/indent */ - -/** - * Lean version of the `Showdown.Side` object used by the official client. - * - * * Basically `Showdown.Side` without the class functions like `addSideCondition()`. - * - * @since 0.1.0 - */ -export type CalcdexLeanSide = Partial, - | 'active' - | 'ally' - | 'battle' - | 'foe' - | 'lastPokemon' - | 'missedPokemon' - | 'pokemon' - | 'wisher' - | 'x' - | 'y' - | 'z' ->>; - -/* eslint-enable @typescript-eslint/indent */ - -export interface CalcdexPlayer extends CalcdexLeanSide { - calcdexNonce?: string; - activeIndex?: number; - selectionIndex?: number; - - /** - * Whether `selectionIndex` should automatically update whenever `activeIndex` updates. - * - * @default true - * @since 0.1.2 - */ - autoSelect?: boolean; - - pokemon?: CalcdexPokemon[]; -} - -export type CalcdexPlayerSide = SmogonState.Side; -export type CalcdexBattleField = SmogonState.Field; - -export type CalcdexPlayerKey = - | 'p1' - | 'p2'; - // | 'p3' - // | 'p4'; - -export type CalcdexPlayerState = Record; - -export interface CalcdexReducerState extends CalcdexPlayerState { - battleId: string; - gen: GenerationNum; - format: string; - field: CalcdexBattleField; -} - -export type CalcdexReducerActionType = - | '@/:init' - | '@/:put' - | '@field/:put' - | '@p1/:init' - | '@p1/:put' - | '@p1/activeIndex:put' - | '@p1/selectionIndex:put' - | '@p1/autoSelect:put' - | '@p1/pokemon:post' - | '@p1/pokemon:put' - | '@p1/pokemon:delete' - | '@p1/pokemon:sync' - | '@p1/side:put' - | '@p2/:init' - | '@p2/:put' - | '@p2/activeIndex:put' - | '@p2/selectionIndex:put' - | '@p2/autoSelect:put' - | '@p2/pokemon:post' - | '@p2/pokemon:put' - | '@p2/pokemon:delete' - | '@p2/pokemon:sync' - | '@p2/side:put'; - -export interface CalcdexReducerAction { - type: CalcdexReducerActionType; - payload?: unknown; -} - -export type CalcdexReducerInstance = React.Reducer; -export type CalcdexReducerDispatch = ThunkyReducerDispatch; - -export const CalcdexInitialState: CalcdexReducerState = { - battleId: null, - gen: 8, - format: null, - field: sanitizeField(null), - - p1: { - sideid: 'p1', - activeIndex: -1, - selectionIndex: 0, - autoSelect: true, - pokemon: [], - }, - - p2: { - sideid: 'p2', - activeIndex: -1, - selectionIndex: 0, - autoSelect: true, - pokemon: [], - }, -}; - -const l = logger('@showdex/pages/Calcdex/CalcdexReducer'); - -const getPlayerKeyFromActionType = ( - type: CalcdexReducerActionType, -): keyof CalcdexReducerState => (type?.startsWith?.('@p2') ? 'p2' : 'p1'); - -export const CalcdexReducer: CalcdexReducerInstance = ( - state, - action, -) => { - l.debug( - 'dispatched', action?.type || '(falsy value)', - '\n', 'payload', action?.payload, - ); - - switch (action?.type) { - case '@/:init': { - const { - battleId, - gen, - format, - } = > (action.payload || {}); - - l.debug( - action.type, - '\n', 'initializing CalcdexReducer state', - '\n', 'battleId', battleId || CalcdexInitialState.battleId, - '\n', 'gen', gen || CalcdexInitialState.gen, - '\n', 'format', format || CalcdexInitialState.format, - ); - - // seems that the dereferenced pointers to each player's `pokemon` array is directly modified, - // which means that the same `pokemon` arrays in `CalcdexInitialState` are modified as well. - // setting each player's `pokemon` to a new array is a gross workaround, but w/e. - - return { - ...CalcdexInitialState, - battleId: battleId || CalcdexInitialState.battleId, - gen: gen || CalcdexInitialState.gen, - format: format || CalcdexInitialState.format, - p1: { - ...CalcdexInitialState.p1, - pokemon: [], - }, - p2: { - ...CalcdexInitialState.p2, - pokemon: [], - }, - }; - } - - case '@/:put': { - const { - battleId, - gen, - format, - } = > (action.payload || {}); - - l.debug( - action.type, - '\n', 'updating battle properties', - '\n', 'battleId', battleId, - '\n', 'gen', gen, 'format', format, - ); - - // since this is a `put` action, don't allow values to be falsy - return { - ...state, - battleId: battleId || state.battleId, - gen: gen && typeof gen === 'number' && gen > 0 ? gen : state.gen, - format: format || state.format, - }; - } - - case '@field/:put': { - const field = (action.payload || {}); - - if (!Object.keys(field).length) { - l.warn( - action.type, - '\n', 'received an empty field payload', - '\n', 'field', field, - ); - - return state; - } - - const updatedField: CalcdexBattleField = { - ...state.field, - ...field, - }; - - return { - ...state, - field: updatedField, - }; - } - - case '@p1/:init': - case '@p2/:init': { - const playerKey = getPlayerKeyFromActionType(action.type); - - return { - ...state, - [playerKey]: CalcdexInitialState[playerKey], - }; - } - - case '@p1/:put': - case '@p2/:put': { - const playerKey = getPlayerKeyFromActionType(action.type); - const player = (action.payload || {}); - - return { - ...state, - [playerKey]: { - ...( state[playerKey]), - ...player, - }, - }; - } - - case '@p1/activeIndex:put': - case '@p2/activeIndex:put': { - const playerKey = getPlayerKeyFromActionType(action.type); - const activeIndex = action.payload; - - l.debug( - action.type, - '\n', 'setting activeIndex of player', playerKey, 'to', activeIndex, - ); - - return { - ...state, - [playerKey]: { - ...( state[playerKey]), - activeIndex: typeof activeIndex === 'number' && activeIndex > -1 ? activeIndex : ( state[playerKey]).activeIndex, - }, - }; - } - - case '@p1/selectionIndex:put': - case '@p2/selectionIndex:put': { - const playerKey = getPlayerKeyFromActionType(action.type); - const selectionIndex = action.payload; - - // if the user manually changes selectionIndex while autoSelect is enabled, then disable it - // const { activeIndex = -1 } = state[playerKey]; - // let { autoSelect } = state[playerKey]; - // - // if (autoSelect && activeIndex > -1 && activeIndex !== selectionIndex) { - // autoSelect = false; - // } - - l.debug( - action.type, - '\n', 'setting selectionIndex of player', playerKey, 'to', selectionIndex, - ); - - return { - ...state, - [playerKey]: { - ...( state[playerKey]), - selectionIndex: typeof selectionIndex === 'number' && selectionIndex > -1 ? selectionIndex : ( state[playerKey]).selectionIndex, - // autoSelect, - }, - }; - } - - case '@p1/autoSelect:put': - case '@p2/autoSelect:put': { - const playerKey = getPlayerKeyFromActionType(action.type); - const autoSelect = action.payload; - - l.debug( - action.type, - '\n', 'setting autoSelect of player', playerKey, 'to', autoSelect, - ); - - return { - ...state, - [playerKey]: { - ...( state[playerKey]), - autoSelect, - }, - }; - } - - case '@p1/pokemon:post': - case '@p2/pokemon:post': { - const playerKey = getPlayerKeyFromActionType(action.type); - const pokemon = > (action.payload || {}); - const ident = detectPokemonIdent(pokemon); - - if (!ident) { - l.warn( - action.type, - '\n', 'received pokemon with invalid ident', ident, - '\n', 'pokemon', pokemon, - ); - - return state; - } - - const { pokemon: updatedPokemon } = state[playerKey]; - - if (!Array.isArray(updatedPokemon)) { - l.warn( - action.type, - '\n', 'found an invalid pokemon array for the player', playerKey, - '\n', `state.${playerKey}.pokemon`, updatedPokemon, - '\n', 'pokemon', pokemon, - ); - - // alternatively, we could just construct an empty array and let the logic continue - return state; - } - - if (updatedPokemon.length >= 6) { - l.warn( - action.type, - '\n', 'player', playerKey, 'cannot have more than 6 pokemon', - '\n', `state.${playerKey}.pokemon`, updatedPokemon, - '\n', 'pokemon', pokemon, - ); - - return state; - } - - // `sanitizePokemon()` will assign a calcdexId and recalculate the calcdexNonce - // (we call this here before the `index` check to ensure a reliable non-falsy `calcdexId`) - const newPokemon = sanitizePokemon(pokemon); - - if (!newPokemon?.calcdexId) { - l.warn( - action.type, - '\n', 'found a falsy calcdexId for the Pokemon', ident, 'despite sanitizing it', - '\n', 'sanitizePokemon(', pokemon, ')', newPokemon, - ); - - return state; - } - - const index = updatedPokemon - .findIndex((p) => (!!p?.calcdexId && p.calcdexId === newPokemon.calcdexId) || detectPokemonIdent(p) === ident); - - if (index > -1) { - l.warn( - action.type, - '\n', 'found a duplicate pokemon', ident, 'for the player', playerKey, - '\n', `state.${playerKey}.pokemon`, updatedPokemon, - '\n', 'pokemon', pokemon, - ); - - return state; - } - - updatedPokemon.push(newPokemon); - - l.debug( - action.type, - '\n', 'adding pokemon', ident, 'for the player', playerKey, - '\n', 'newPokemon', newPokemon, - '\n', `state.${playerKey}.pokemon`, updatedPokemon, - ); - - return { - ...state, - [playerKey]: { - ...( state[playerKey]), - pokemon: updatedPokemon, - }, - }; - } - - case '@p1/pokemon:put': - case '@p1/pokemon:sync': - case '@p2/pokemon:put': - case '@p2/pokemon:sync': { - const playerKey = getPlayerKeyFromActionType(action.type); - const pokemon = > (action.payload || {}); - const ident = detectPokemonIdent(pokemon); - - l.debug( - action.type, - '\n', 'playerKey', playerKey, - '\n', 'pokemon', pokemon, - '\n', 'ident', ident, - ); - - if (!ident) { - l.warn( - action.type, - '\n', 'received pokemon with invalid ident', ident, - '\n', 'pokemon', pokemon, - ); - - return state; - } - - const { - pokemon: updatedPokemon, - } = state[playerKey]; - - if (!Array.isArray(updatedPokemon)) { - l.warn( - action.type, - '\n', 'found an invalid pokemon array for the player', playerKey, - '\n', `state.${playerKey}.pokemon`, updatedPokemon, - '\n', 'pokemon', pokemon, - ); - - return state; - } - - const index = updatedPokemon - .findIndex((p) => (!!p?.calcdexId && p.calcdexId === pokemon?.calcdexId) || detectPokemonIdent(p) === ident); - - if (index < 0) { - l.warn( - action.type, - '\n', 'found no pokemon', ident, 'for the player', playerKey, - '\n', `state.${playerKey}.pokemon`, updatedPokemon, - '\n', 'pokemon', pokemon, - '\n', 'index', index, - ); - - return state; - } - - // `syncPokemon()` will assign a calcdexId and recalculate the calcdexNonce - const shouldSync = action.type.endsWith(':sync'); - - const syncedPokemon = shouldSync ? - syncPokemon(updatedPokemon[index], pokemon) : // @p#/pokemon:sync - { // @p#/pokemon:put - ...updatedPokemon[index], - ...pokemon, - - // if abilityToggled exists in the payload, probably was user-invoked, so use that value - // (otherwise, automatically determine the value based on the Pokemon). - // omitting this check would prevent the user from manually toggling the ability. - abilityToggled: typeof pokemon?.abilityToggled === 'boolean' ? - pokemon.abilityToggled : - detectToggledAbility(pokemon), - - dirtyBoosts: { - ...updatedPokemon[index].dirtyBoosts, - ...pokemon?.dirtyBoosts, - }, - }; - - if (!shouldSync) { - if (!syncedPokemon.calcdexId) { - syncedPokemon.calcdexId = calcPokemonCalcdexId(syncedPokemon); - } - - syncedPokemon.calcdexNonce = calcPokemonCalcdexNonce(syncedPokemon); - } - - l.debug( - action.type, - '\n', 'comparing current Pokemon', ident, 'calcdexNonce', updatedPokemon[index].calcdexNonce, - '\n', 'updatedPokemon[', index, ']', updatedPokemon[index], - '\n', 'with the Pokemon\'s recalculated calcdexNonce', syncedPokemon.calcdexNonce, - '\n', 'syncedPokemon', syncedPokemon, - ); - - if (syncedPokemon?.calcdexNonce === updatedPokemon[index].calcdexNonce) { - l.debug( - action.type, - '\n', 'no updates will be made for the Pokemon', ident, - '\n', 'since its calcdexNonce did not change, even after recalculation', - '\n', 'current calcdexNonce', updatedPokemon[index].calcdexNonce, updatedPokemon[index], - '\n', 'recalculated calcdexNonce', syncedPokemon.calcdexNonce, syncedPokemon, - ); - - return state; - } - - updatedPokemon[index] = syncedPokemon; - - l.debug( - action.type, - '\n', 'updating Pokemon', ident, 'for the player', playerKey, - '\n', 'current calcdexNonce', updatedPokemon[index].calcdexNonce, updatedPokemon[index], - '\n', 'recalculated calcdexNonce', syncedPokemon.calcdexNonce, syncedPokemon, - '\n', `state.${playerKey}.pokemon[`, index, ']', updatedPokemon[index], - '\n', `state.${playerKey}.pokemon`, updatedPokemon, - ); - - return { - ...state, - [playerKey]: { - ...( state[playerKey]), - pokemon: updatedPokemon, - }, - }; - } - - case '@p1/pokemon:delete': - case '@p2/pokemon:delete': { - const playerKey = getPlayerKeyFromActionType(action.type); - const pokemon = > action.payload; - const ident = detectPokemonIdent(pokemon); - - if (!ident) { - l.warn( - action.type, - '\n', 'received pokemon with invalid ident', ident, - '\n', 'pokemon', pokemon, - ); - - return state; - } - - const { pokemon: updatedPokemon } = state[playerKey]; - - if (!Array.isArray(updatedPokemon)) { - l.warn( - action.type, - '\n', 'found an invalid pokemon array for the player', playerKey, - '\n', `state.${playerKey}.pokemon`, updatedPokemon, - '\n', 'pokemon', pokemon, - ); - - return state; - } - - const index = updatedPokemon.findIndex((p) => p?.ident === ident); - - if (index < 0) { - l.warn( - action.type, - '\n', 'found no pokemon', ident, 'for the player', playerKey, - '\n', `state.${playerKey}.pokemon`, updatedPokemon, - '\n', 'pokemon', pokemon, - '\n', 'index', index, - ); - - return state; - } - - const deletedPokemon = updatedPokemon.splice(index, 1); - - l.debug( - action.type, - '\n', 'deleting pokemon', ident, 'for the player', playerKey, - '\n', `state.${playerKey}.pokemon[index`, index, ']', deletedPokemon, - '\n', `state.${playerKey}.pokemon`, updatedPokemon, - ); - - return { - ...state, - [playerKey]: { - ...( state[playerKey]), - pokemon: updatedPokemon, - }, - }; - } - - case '@p1/side:put': - case '@p2/side:put': { - const playerKey = getPlayerKeyFromActionType(action.type); - const clientSide = (action.payload || {}); - - if (!clientSide?.sideid) { - l.warn( - action.type, - '\n', 'received clientSide with an invalid sideid', clientSide?.sideid, - '\n', 'clientSide', clientSide, - ); - - return state; - } - - const { activeIndex, selectionIndex } = state[playerKey]; - const updatedSide = sanitizePlayerSide(clientSide); - - updatedSide.isSwitching = activeIndex === selectionIndex ? 'out' : 'in'; - - l.debug( - action.type, - '\n', 'updating side', clientSide?.sideid, 'for the player', playerKey, - '\n', `state.${playerKey}.side`, updatedSide, - ); - - return { - ...state, - [playerKey]: { - ...( state[playerKey]), - side: updatedSide, - }, - }; - } - - default: { - l.warn('unknown action type', action?.type || '(falsy value)', action); - - return state; - } - } -}; diff --git a/src/pages/Calcdex/fetchPokemonMovesets.ts b/src/pages/Calcdex/fetchPokemonMovesets.ts deleted file mode 100644 index 473152d1..00000000 --- a/src/pages/Calcdex/fetchPokemonMovesets.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { logger } from '@showdex/utils/debug'; -import type { Generation as PkmnGeneration } from '@pkmn/data'; -import type { CalcdexPokemon } from './CalcdexReducer'; -import { detectPokemonIdent } from './detectPokemonIdent'; -import { detectSpeciesForme } from './detectSpeciesForme'; - -const l = logger('@showdex/pages/Calcdex/fetchPokemonMovesets'); - -export const fetchPokemonMovesets = async ( - dex: PkmnGeneration, - pokemon: Partial, - format?: string, -): Promise> => { - const ident = detectPokemonIdent(pokemon); - const speciesForme = detectSpeciesForme(pokemon); - - const newPokemon: Partial = { - moveState: { - revealed: pokemon.moveTrack?.map?.((m) => m?.[0]).filter(Boolean) - ?? pokemon.moveState?.revealed - ?? [], - learnset: pokemon.moveState?.learnset ?? [], - other: pokemon.moveState?.other ?? [], - }, - }; - - if (!speciesForme) { - l.warn( - 'fetchPokemonMovesets() <- detectSpeciesForme()', - '\n', 'failed to detect speciesForme from Pokemon with ident', ident, - '\n', 'speciesForme', speciesForme, - '\n', 'pokemon', pokemon, - ); - - return newPokemon; - } - - if (typeof dex?.learnsets?.learnable === 'function') { - // l.debug( - // 'fetchPokemonMovesets() -> await dex.learnsets.learnable()', - // '\n', 'speciesForme', speciesForme, - // '\n', 'ident', ident, - // ); - - const learnset = await dex.learnsets.learnable(speciesForme); - - newPokemon.moveState.learnset = Object.keys(learnset || {}) - .map((moveid) => dex.moves.get(moveid)?.name) - .filter((name) => !!name && !newPokemon.moveState.revealed.includes(name)) - .sort(); - - // l.debug( - // 'fetchPokemonMovesets() <- await dex.learnsets.learnable()', - // '\n', 'speciesForme', speciesForme, - // '\n', 'learnset for', ident, 'set to', newPokemon.moveState.learnset, - // ); - } - - // build `other`, only if we have no `learnsets` or the `format` has something to do with hacks - if (!newPokemon.moveState.learnset.length || (format && /anythinggoes|hackmons/i.test(format))) { - // l.debug( - // 'fetchPokemonPresets() -> BattleMovedex', - // '\n', 'building other movesets list from client BattleMovedex object', - // '\n', 'moveState', newPokemon.moveState, - // '\n', 'format', format, - // '\n', 'ident', ident, - // ); - - newPokemon.moveState.other = Object.keys(BattleMovedex) - .map((moveid) => dex.moves.get(moveid)?.name) - .filter((name) => !!name && !newPokemon.moveState.revealed.includes(name) && !newPokemon.moveState.learnset?.includes?.(name)) - .sort(); - - // l.debug( - // 'fetchPokemonMovesets() <- BattleMovedex', - // '\n', 'newPokemon.moveState.other', newPokemon.moveState.other, - // '\n', 'ident', ident, - // ); - } - - // l.debug( - // 'fetchPokemonMovesets() -> return newPokemon', - // '\n', 'newPokemon', newPokemon, - // ); - - return newPokemon; -}; diff --git a/src/pages/Calcdex/fetchPokemonPresets.ts b/src/pages/Calcdex/fetchPokemonPresets.ts deleted file mode 100644 index 9862dc1a..00000000 --- a/src/pages/Calcdex/fetchPokemonPresets.ts +++ /dev/null @@ -1,124 +0,0 @@ -// import { logger } from '@showdex/utils/debug'; -import type { Generation } from '@pkmn/data'; -import type { CalcdexPokemon } from './CalcdexReducer'; -import type { PresetCacheHookInterface } from './usePresetCache'; -// import { calcPokemonStats } from './calcPokemonStats'; -// import { detectPokemonIdent } from './detectPokemonIdent'; -import { detectSpeciesForme } from './detectSpeciesForme'; - -// const l = logger('@showdex/pages/Calcdex/fetchPokemonPresets'); - -export const fetchPokemonPresets = async ( - _dex: Generation, /** @todo refactor this out since it's no longer being used */ - cache: PresetCacheHookInterface, - pokemon: Partial, - format: string, -): Promise> => { - // const ident = detectPokemonIdent(pokemon); - const speciesForme = detectSpeciesForme(pokemon)?.replace?.(/-Mega/gi, ''); - - const newPokemon: Partial = { - // speciesForme, // required for calcPokemonStats() - - altAbilities: pokemon?.altAbilities ?? [], - altItems: pokemon?.altItems ?? [], - altMoves: pokemon?.altMoves ?? [], - - presets: pokemon?.presets ?? [], - }; - - if (!newPokemon.presets.length) { - const newPresets = cache.get(format, speciesForme); - - newPokemon.autoPreset = !!newPresets?.length; - - if (newPresets?.length) { - newPokemon.presets.push(...newPresets); - - if (pokemon.autoPreset) { - const [firstPreset] = newPokemon.presets; - - // l.debug( - // 'auto-setting preset for Pokemon', ident, 'to', firstPreset?.name, - // '\n', 'calcdexId', firstPreset?.calcdexId, - // '\n', 'firstPreset', firstPreset, - // '\n', 'newPokemon', newPokemon, - // ); - - newPokemon.preset = firstPreset?.calcdexId; - - if (firstPreset?.item) { - // newPokemon.item = firstPreset.item; - // newPokemon.dirtyItem = firstPreset.item; - - if (pokemon?.item && pokemon.item !== '(exists)' && pokemon.item !== firstPreset.item) { - newPokemon.dirtyItem = firstPreset.item; - } else { - newPokemon.item = firstPreset.item; - } - - newPokemon.altItems = firstPreset.altItems; - } - - if (firstPreset?.ability) { - // for these formats, probably not even a legal ability, so set it as the dirtyAbility - if (['AAA', 'AG', 'BH'].includes(firstPreset.name.split(' ')[0])) { - newPokemon.dirtyAbility = firstPreset.ability; - } else { - newPokemon.ability = firstPreset.ability; - } - - newPokemon.altAbilities = firstPreset.altAbilities; - } - - if (firstPreset?.nature) { - newPokemon.nature = firstPreset.nature; - } - - if (firstPreset.moves?.length) { - newPokemon.moves = firstPreset.moves; - newPokemon.altMoves = firstPreset.altMoves; - } - - if (Object.keys(firstPreset?.ivs || {}).length) { - newPokemon.ivs = { ...newPokemon.ivs, ...firstPreset.ivs }; - } - - if (Object.keys(firstPreset?.evs || {}).length) { - newPokemon.evs = { ...newPokemon.evs, ...firstPreset.evs }; - } - - // l.debug( - // 'auto-set complete for Pokemon', ident, - // '\n', 'newPokemon.preset', newPokemon.preset, - // '\n', 'newPokemon', newPokemon, - // ); - } - } - } - - // l.debug( - // 'fetchPokemonPresets() -> calcPokemonStats()', - // '\n', 'newPokemon', newPokemon, - // '\n', 'ident', ident, - // ); - - // calculate the stats based on what we know atm - // update (2022/03/10): calculatedStats is now being calculated (and memoized) on the fly in PokeCalc - // newPokemon.calculatedStats = calcPokemonStats(dex, newPokemon); - - // l.debug( - // 'fetchPokemonPresets() <- calcPokemonStats()', - // '\n', 'calculatedStats', newPokemon.calculatedStats, - // '\n', 'newPokemon', newPokemon, - // '\n', 'ident', ident, - // ); - - // l.debug( - // 'fetchPokemonPresets() -> return newPokemon', - // '\n', 'newPokemon', newPokemon, - // '\n', 'ident', ident, - // ); - - return newPokemon; -}; diff --git a/src/pages/Calcdex/sanitizeSpeciesForme.ts b/src/pages/Calcdex/sanitizeSpeciesForme.ts deleted file mode 100644 index 371475dd..00000000 --- a/src/pages/Calcdex/sanitizeSpeciesForme.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Removes crap like wildcard formes from the Pokemon's `speciesForme`. - * - * The following is a list of edge cases that made the extension crap itself: - * * *-Mega (e.g., Lopunny-Mega -> Lopunny) - * * *-Gmax (e.g., Alcremie-Gmax -> Alcremie) - * * *-Original (e.g., Magerna-Original -> Magerna) - * * Gastrodon-East -> Gastrodon - * * Genesect-Burn -> Genesect - * * Genesect-Chill -> Genesect - * * Genesect-Douse -> Genesect - * * Genesect-Shock -> Genesect - * * Urshifu-Rapid-Strike -> Urshifu - * * Zygarde-10% -> Zygarde - * * Zygarde-Complete -> Zygarde - * - * @example 'Urshifu-*' -> 'Urshifu' - * @since 0.1.0 - */ -export const sanitizeSpeciesForme = ( - speciesForme: string, -): string => speciesForme?.replace?.( - /-(?:\*|Mega|Gmax|Original|East|Burn|Chill|Douse|Shock|Rapid-Strike|10%|Complete)/gi, - '', -); diff --git a/src/pages/Calcdex/syncServerPokemon.ts b/src/pages/Calcdex/syncServerPokemon.ts deleted file mode 100644 index 634fe4eb..00000000 --- a/src/pages/Calcdex/syncServerPokemon.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { logger } from '@showdex/utils/debug'; -import type { Generation } from '@pkmn/data'; -import type { CalcdexPokemon, CalcdexPokemonPreset } from './CalcdexReducer'; -import type { PresetCacheHookInterface } from './usePresetCache'; -import { calcPresetCalcdexId } from './calcCalcdexId'; -// import { calcPokemonStats } from './calcPokemonStats'; -import { detectPokemonIdent } from './detectPokemonIdent'; -import { detectSpeciesForme } from './detectSpeciesForme'; -import { guessServerSpread } from './guessServerSpread'; - -const l = logger('@showdex/pages/Calcdex/syncServerPokemon'); - -export const syncServerPokemon = ( - dex: Generation, - cache: PresetCacheHookInterface, - format: string, - clientPokemon: Partial, - serverPokemon: Showdown.ServerPokemon, -): CalcdexPokemon => { - if (!serverPokemon?.stats) { - l.debug( - 'serverPokemon has no stats object', - '\n', 'dex', dex, - '\n', 'clientPokemon', clientPokemon, - '\n', 'serverPokemon', serverPokemon, - ); - - return clientPokemon; - } - - const isRandom = format?.includes?.('random'); - - const syncedPokemon: CalcdexPokemon = { - ...clientPokemon, - serverSourced: true, - - name: serverPokemon.name ?? clientPokemon?.name, - level: serverPokemon.level ?? clientPokemon?.level, - - ability: serverPokemon?.ability ?? clientPokemon?.ability, - moves: [...( serverPokemon?.moves ?? clientPokemon?.moves ?? [])], - - // this will bypass `@p${#}/pokemon:sync` (i.e., `@p${#}/pokemon:put`), - // so we need to specify this, otherwise there will be X's in the UI - boosts: { - atk: clientPokemon?.boosts?.atk ?? 0, - def: clientPokemon?.boosts?.def ?? 0, - spa: clientPokemon?.boosts?.spa ?? 0, - spd: clientPokemon?.boosts?.spd ?? 0, - spe: clientPokemon?.boosts?.spe ?? 0, - }, - }; - - syncedPokemon.ident = detectPokemonIdent(syncedPokemon); - syncedPokemon.speciesForme = detectSpeciesForme(syncedPokemon); - - if (!syncedPokemon.speciesForme) { - l.debug( - 'syncServerPokemon() <- detectSpeciesForme()', - '\n', 'received an invalid speciesForme', - '\n', 'speciesForme', syncedPokemon.speciesForme, - '\n', 'syncedPokemon', syncedPokemon, - '\n', 'clientPokemon', clientPokemon, - '\n', 'serverPokemon', serverPokemon, - ); - - return clientPokemon; - } - - // build a preset based on the serverPokemon's stats - const serverPreset: CalcdexPokemonPreset = { - name: isRandom ? 'Randoms' : 'Yours', - level: syncedPokemon.level, - gender: syncedPokemon.gender, - ivs: {}, - evs: {}, - }; - - // all Pokemon in randoms have a Hardy nature w/ 31 IVs (unless specified) & 84 EVs - if (isRandom) { - const [randomPreset] = cache.get(format, syncedPokemon.speciesForme); - - serverPreset.ivs = { - hp: 31, - atk: 31, - def: 31, - spa: 31, - spd: 31, - spe: 31, - }; - - serverPreset.evs = { - hp: 84, - atk: 84, - def: 84, - spa: 84, - spd: 84, - spe: 84, - }; - - serverPreset.nature = 'Hardy'; - - if (randomPreset) { - serverPreset.ivs = { ...serverPreset.ivs, ...randomPreset?.ivs }; - serverPreset.evs = { ...serverPreset.evs, ...randomPreset?.evs }; - serverPreset.altAbilities = randomPreset?.altAbilities; - serverPreset.altItems = randomPreset?.altItems; - serverPreset.altMoves = randomPreset?.altMoves; - } - } else { - const guessedSpread = guessServerSpread(dex, syncedPokemon, serverPokemon); - - serverPreset.nature = guessedSpread?.nature; - serverPreset.ivs = { ...guessedSpread?.ivs }; - serverPreset.evs = { ...guessedSpread?.evs }; - } - - if (serverPreset.nature) { - syncedPokemon.nature = serverPreset.nature; - } - - if (Object.keys(serverPreset.ivs).length) { - syncedPokemon.ivs = { ...serverPreset.ivs }; - } - - if (Object.keys(serverPreset.evs).length) { - syncedPokemon.evs = { ...serverPreset.evs }; - } - - const serverAbility = serverPokemon?.ability ? - dex.abilities.get(serverPokemon.ability) : - null; - - if (serverAbility?.name) { - // since we know the actual ability, no need to set it as a dirtyAbility - serverPreset.ability = serverAbility.name; - syncedPokemon.ability = serverAbility.name; - } - - const serverItem = serverPokemon?.item ? - dex.items.get(serverPokemon.item) : - null; - - if (serverItem?.name) { - // same goes for the item (as with the case of the ability); no need for dirtyItem - serverPreset.item = serverItem.name; - syncedPokemon.item = serverItem.name; - } - - if (serverPokemon?.moves?.length) { - // e.g., serverPokemon.moves = ['calmmind', 'moonblast', 'flamethrower', 'thunderbolt'] - // what we want: ['Calm Mind', 'Moonblast', 'Flamethrower', 'Thunderbolt'] - serverPreset.moves = serverPokemon.moves.map((moveName) => { - const move = dex.moves.get(moveName); - - if (!move?.name) { - return null; - } - - return move.name; - }).filter(Boolean); - - syncedPokemon.moves = [...serverPreset.moves]; - } - - serverPreset.calcdexId = calcPresetCalcdexId(serverPreset); - - if (!Array.isArray(syncedPokemon?.presets)) { - syncedPokemon.presets = []; - } - - syncedPokemon.presets.unshift(serverPreset); - syncedPokemon.preset = serverPreset.calcdexId; - syncedPokemon.autoPreset = true; - - // l.debug( - // 'syncServerPokemon() -> return syncedPokemon', - // '\n', 'syncedPokemon', syncedPokemon, - // '\n', 'format', format, - // '\n', 'clientPokemon', clientPokemon, - // '\n', 'serverPokemon', serverPokemon, - // ); - - return syncedPokemon; -}; diff --git a/src/pages/Calcdex/usePresetCache.ts b/src/pages/Calcdex/usePresetCache.ts deleted file mode 100644 index b52cf790..00000000 --- a/src/pages/Calcdex/usePresetCache.ts +++ /dev/null @@ -1,430 +0,0 @@ -import * as React from 'react'; -import { FormatLabels } from '@showdex/consts'; -import { runtimeFetch } from '@showdex/utils/core'; -import { logger } from '@showdex/utils/debug'; -import type { - AbilityName, - ItemName, - MoveName, -} from '@pkmn/data'; -import type { CalcdexPokemonPreset } from './CalcdexReducer'; -import { calcPresetCalcdexId } from './calcCalcdexId'; -import { detectGenFromFormat } from './detectGenFromFormat'; -import { sanitizeSpeciesForme } from './sanitizeSpeciesForme'; - -export interface PkmnSmogonPreset { - ability: AbilityName | AbilityName[]; - nature: Showdown.PokemonNature | Showdown.PokemonNature[]; - item: ItemName | ItemName[]; - moves: (MoveName | MoveName[])[]; - ivs?: Showdown.StatsTable; - evs?: Showdown.StatsTable; -} - -export type PkmnSmogonPresetFormats = { - [format: string]: { - [presetName: string]: PkmnSmogonPreset; - }; -}; - -export type PkmnSmogonPresets = { - [speciesForme: string]: PkmnSmogonPresetFormats; -}; - -export interface PkmnSmogonRandomPreset { - level: number; - abilities: AbilityName[]; - items: ItemName[]; - moves: MoveName[]; - ivs?: Showdown.StatsTable; - evs?: Showdown.StatsTable; -} - -export type PkmnSmogonRandomPresets = { - [speciesForme: string]: PkmnSmogonRandomPreset; -}; - -export type PkmnSmogonPresetCache = { - [genName: string]: { - [speciesForme: string]: CalcdexPokemonPreset[]; - }; -}; - -export interface PresetCacheHookInterface { - loading: boolean; - available: (format: string) => boolean; - fetch: (format: string, force?: boolean) => Promise; - get: (format: string, speciesForme: string) => CalcdexPokemonPreset[]; - purge: () => void; -} - -const l = logger('@showdex/pages/Calcdex/usePresetCache'); - -export const usePresetCache = (): PresetCacheHookInterface => { - const [presetCache, setPresetCache] = React.useState({}); - const [loading, setLoading] = React.useState(false); - - const available: PresetCacheHookInterface['available'] = (format) => { - if (!format) { - l.warn( - 'available()', - '\n', 'something something you forgot the format lmao', - '\n', 'format', format, - ); - - return false; - } - - const isRandom = format.includes('random'); - const genName = isRandom ? format : `gen${detectGenFromFormat(format)}`; - - return !!Object.keys(presetCache[genName] || {}).length; - }; - - const fetch: PresetCacheHookInterface['fetch'] = async (format, force) => { - if (!format) { - l.warn( - 'fetch()', - '\n', 'you forgot to specify the format dummy', - '\n', 'format', format, - ); - - return; - } - - const isRandom = format.includes('random'); - const genName = isRandom ? format : `gen${detectGenFromFormat(format)}`; - - if (available(format) && !force) { - l.debug( - 'fetch()', - '\n', 'seems like the presets for this', isRandom ? 'format' : 'gen', 'have been fetched already', - '\n', '(if you want to fetch them again, pass `true` for the second `force` argument)', - '\n', 'format', format, - '\n', 'genName', genName, - '\n', 'presetCache', presetCache, - ); - - return; - } - - const baseUrl = isRandom ? - process.env.SMOGON_RANDOM_PRESETS_URL : - process.env.SMOGON_PRESETS_URL; - - const url = `${baseUrl}/${genName}.json`; - - l.debug( - 'fetch() -> await runtimeFetch()', - '\n', 'url', url, - ); - - setLoading(true); - - let downloadedData: Record>> = {}; - - const response = await runtimeFetch(url); - // const data = await response.json(); - downloadedData = await response.json(); - - l.debug( - 'fetch() <- await runtimeFetch()', - '\n', 'downloadedData', downloadedData, - ); - - if (!Object.keys(downloadedData || {}).length) { - l.warn( - 'o snap! no presets bruh', - '\n', 'isRandom?', isRandom, - '\n', 'genName', genName, - '\n', 'format', format, - ); - - return; - } - - // gen8bdsp* is a format that requires some additional sets from gen4 - // (otherwise, some Pokemon [like Breloom] won't have any sets since they doesn't exist in gen8) - if (!isRandom && format.startsWith('gen8bdsp')) { - const gen4Url = `${baseUrl}/gen4.json`; - - l.debug( - 'fetch() -> await runtimeFetch()', - '\n', 'downloading additional presets from gen4 since format is gen8bdsp* (not random)', - '\n', 'gen4Url', gen4Url, - '\n', 'format', format, - ); - - const gen4Response = await runtimeFetch(gen4Url); - const gen4Data = await gen4Response.json(); - - l.debug( - 'fetch() <- await runtimeFetch()', - '\n', 'gen4Data', gen4Data, - ); - - // inject the presets into what already have; otherwise, we'll overwrite existing ones! - Object.entries(gen4Data).forEach(([ - forme, - formats, - ]: [ - forme: string, - formats: Record>, - ]) => { - if (!(forme in downloadedData)) { - downloadedData[forme] = formats; - - return; - } - - Object.entries(formats).forEach(([currentFormat, presets]) => { - if (!(currentFormat in downloadedData[forme])) { - downloadedData[forme][currentFormat] = presets; - - return; - } - - downloadedData[forme][currentFormat] = { - ...downloadedData[forme][currentFormat], - ...presets, - }; - }); - }); - - l.debug( - 'post gen4Data injection into downloadedData', - '\n', 'downloadedData', downloadedData, - ); - } - - return new Promise((resolve) => { - setPresetCache((prevPresetCache) => { - // every format will be under `gen<#>`, except for randoms - if (!(genName in prevPresetCache)) { - prevPresetCache[genName] = {}; - } - - Object.entries(downloadedData).forEach(([forme, value]) => { - const sanitizedForme = sanitizeSpeciesForme(forme); - - if (!Array.isArray(prevPresetCache[genName][sanitizedForme])) { - prevPresetCache[genName][sanitizedForme] = []; - } - - if (isRandom) { - // literally redeclared just for TypeScript lmao - const preset = value; - - const calcdexPreset: CalcdexPokemonPreset = { - name: 'Randoms', - species: forme, // purposefully not sanitized - level: preset?.level, - - ability: preset?.abilities?.[0], - altAbilities: preset?.abilities, - - // seems that all Pokemon have the Hardy nature - // (according to https://calc.pokemonshowdown.com/randoms.html) - nature: 'Hardy', - - item: preset?.items?.[0], - altItems: preset?.items, - - moves: preset?.moves?.slice?.(0, 4), - altMoves: preset?.moves, - - ivs: { - hp: preset?.ivs?.hp ?? 31, - atk: preset?.ivs?.atk ?? 31, - def: preset?.ivs?.def ?? 31, - spa: preset?.ivs?.spa ?? 31, - spd: preset?.ivs?.spd ?? 31, - spe: preset?.ivs?.spe ?? 31, - }, - - // all EVs default to 84 - // (according to https://calc.pokemonshowdown.com/randoms.html) - evs: { - hp: preset?.evs?.hp ?? 84, - atk: preset?.evs?.atk ?? 84, - def: preset?.evs?.def ?? 84, - spa: preset?.evs?.spa ?? 84, - spd: preset?.evs?.spd ?? 84, - spe: preset?.evs?.spe ?? 84, - }, - }; - - calcdexPreset.calcdexId = calcPresetCalcdexId(calcdexPreset); - - const index = prevPresetCache[genName][sanitizedForme] - .findIndex((p) => p.calcdexId === calcdexPreset.calcdexId); - - if (index > -1) { - prevPresetCache[genName][sanitizedForme][index] = calcdexPreset; - } else { - prevPresetCache[genName][sanitizedForme].push(calcdexPreset); - } - } else { - Object.entries( value).forEach(([currentFormat, presets]) => { - Object.entries(presets).forEach(([presetName, preset]) => { - const formatLabel = currentFormat in FormatLabels ? - FormatLabels[currentFormat] : - currentFormat?.toUpperCase?.(); - - const altMoves = preset?.moves?.flatMap?.((move) => move) ?? []; - - const calcdexPreset: CalcdexPokemonPreset = { - name: `${formatLabel} ${presetName}`, // e.g., 'OU Defensive Pivot' - species: forme, // purposefully not sanitized - - ability: Array.isArray(preset?.ability) ? preset.ability[0] : preset?.ability, - altAbilities: Array.isArray(preset?.ability) ? preset.ability : [preset?.ability].filter(Boolean), - - nature: Array.isArray(preset?.nature) ? preset.nature[0] : preset?.nature, - - item: Array.isArray(preset?.item) ? preset.item[0] : preset?.item, - altItems: Array.isArray(preset?.item) ? preset.item : [preset?.item].filter(Boolean), - - moves: preset?.moves?.map?.((move) => (Array.isArray(move) ? move[0] : move)) ?? [], - altMoves: altMoves.filter((m, i) => !altMoves.includes(m, i + 1)), // remove duplicate moves - - ivs: { - hp: preset?.ivs?.hp ?? 31, - atk: preset?.ivs?.atk ?? 31, - def: preset?.ivs?.def ?? 31, - spa: preset?.ivs?.spa ?? 31, - spd: preset?.ivs?.spd ?? 31, - spe: preset?.ivs?.spe ?? 31, - }, - - evs: preset?.evs, - }; - - calcdexPreset.calcdexId = calcPresetCalcdexId(calcdexPreset); - - const index = prevPresetCache[genName][sanitizedForme] - .findIndex((p) => p.calcdexId === calcdexPreset.calcdexId); - - if (index > -1) { - prevPresetCache[genName][sanitizedForme][index] = calcdexPreset; - } else { - prevPresetCache[genName][sanitizedForme].push(calcdexPreset); - } - }); - }); - } - }); - - l.debug( - 'fetch()', - '\n', 'finished processing and caching presets from Smogon', - '\n', 'presetCache[', genName, ']', prevPresetCache[genName], - '\n', 'presetCache', prevPresetCache, - '\n', 'isRandom?', isRandom, - '\n', 'genName', genName, - '\n', 'format', format, - ); - - setLoading(false); - resolve(); - - return prevPresetCache; - }); - }); - }; - - const get: PresetCacheHookInterface['get'] = (format, speciesForme) => { - if (!format) { - l.warn( - 'get()', - '\n', 'you forgot to specify the format dum dum', - '\n', 'format', format, - '\n', 'speciesForme', speciesForme, - ); - - return []; - } - - const isRandom = format.includes('random'); - const genName = isRandom ? format : `gen${detectGenFromFormat(format)}`; - - if (!speciesForme) { - l.warn( - 'get()', - '\n', 'need a mon (speciesForme) to pull them presets bruh', - '\n', 'isRandom?', isRandom, - '\n', 'genName', genName, - '\n', 'format', format, - '\n', 'speciesForme', speciesForme, - ); - - return []; - } - - const sanitizedSpeciesForme = sanitizeSpeciesForme(speciesForme); - - if (!(sanitizedSpeciesForme in (presetCache[genName] ?? {}))) { - l.debug( - 'get()', - '\n', 'no presets for the Pokemon', sanitizedSpeciesForme, 'sadge :(', - '\n', 'isRandom?', isRandom, - '\n', 'genName', genName, - '\n', 'format', format, - '\n', 'speciesForme', speciesForme, - ); - - return []; - } - - const baseGen = `gen${detectGenFromFormat(format)}`; - const genlessFormat = format.replace(baseGen, ''); - - const formatLabel = genlessFormat in FormatLabels ? - FormatLabels[genlessFormat] : - genlessFormat.toUpperCase(); - - // put the presets in the current tier first, then the rest - const presets = presetCache[genName][sanitizedSpeciesForme].sort((a, b) => { - // prevents something like 'OU-2X' being sorted before 'OU' - const formatSearchString = `${formatLabel} `; - - if (a.name.startsWith(formatSearchString)) { - return -1; - } - - if (b.name.startsWith(formatSearchString)) { - return 1; - } - - return 0; - }); - - l.debug( - 'found cached presets for Pokemon', sanitizedSpeciesForme, - '\n', 'presets', presets, - '\n', 'isRandom?', isRandom, - '\n', 'genName', genName, - '\n', 'format', format, - '\n', 'speciesForme', speciesForme, - ); - - return presets; - }; - - const purge: PresetCacheHookInterface['purge'] = () => { - l.debug( - 'purge()', - '\n', 'preset cache is going bye bye', - ); - - setPresetCache({}); - }; - - return { - loading, - available, - fetch, - get, - purge, - }; -}; From 82ba4fc11d344d340662c18c663466a946929e88 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Fri, 12 Aug 2022 23:15:12 -0700 Subject: [PATCH 020/143] feat(calcdex): added redux provider to bootstrapper --- src/pages/Calcdex/Calcdex.bootstrap.tsx | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/pages/Calcdex/Calcdex.bootstrap.tsx b/src/pages/Calcdex/Calcdex.bootstrap.tsx index 028a4011..8c64f9d2 100644 --- a/src/pages/Calcdex/Calcdex.bootstrap.tsx +++ b/src/pages/Calcdex/Calcdex.bootstrap.tsx @@ -1,18 +1,20 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; +import { Provider as ReduxProvider } from 'react-redux'; import { ColorSchemeProvider } from '@showdex/components/app'; import { createSideRoom, // getActiveBattle, getBattleRoom, } from '@showdex/utils/app'; +import { calcBattleCalcdexNonce } from '@showdex/utils/calc'; import { logger } from '@showdex/utils/debug'; -import { calcBattleCalcdexNonce } from './calcCalcdexNonce'; +import type { RootStore } from '@showdex/redux/store'; import { Calcdex } from './Calcdex'; const l = logger('@showdex/pages/Calcdex/Calcdex.bootstrap'); -export const bootstrap = (roomid?: string): void => { +export const bootstrap = (store: RootStore, roomid?: string): void => { l.debug( 'Calcdex bootstrapper was invoked;', 'determining if there\'s anything to do...', @@ -34,7 +36,7 @@ export const bootstrap = (roomid?: string): void => { const { battle, - tooltips, + // tooltips, } = getBattleRoom(roomid); if (!battle?.id) { @@ -142,15 +144,18 @@ export const bootstrap = (roomid?: string): void => { l.debug( 'battle.subscribe() -> activeBattle.reactCalcdexRoom.render()', '\n', 'rendering Calcdex with battle nonce', activeBattle.nonce, + '\n', 'store.getState()', store.getState(), ); activeBattle.reactCalcdexRoom.render(( - - - + + + + + )); }); From d333864c33e4031d0266dfe41a1df623a41919d6 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Fri, 12 Aug 2022 23:16:32 -0700 Subject: [PATCH 021/143] feat(calcdex): added redux integration in react components --- src/pages/Calcdex/Calcdex.tsx | 39 +++++++++----- src/pages/Calcdex/FieldCalc.tsx | 4 +- src/pages/Calcdex/PlayerCalc.tsx | 12 ++--- src/pages/Calcdex/PokeCalc.tsx | 88 +++++++++++++++++++------------- src/pages/Calcdex/PokeMoves.tsx | 32 ++++++------ src/pages/Calcdex/PokeStats.tsx | 9 ++-- 6 files changed, 106 insertions(+), 78 deletions(-) diff --git a/src/pages/Calcdex/Calcdex.tsx b/src/pages/Calcdex/Calcdex.tsx index 3611ceb6..c7880a00 100644 --- a/src/pages/Calcdex/Calcdex.tsx +++ b/src/pages/Calcdex/Calcdex.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import cx from 'classnames'; import { useColorScheme } from '@showdex/components/app'; +import { detectPlayerKeyFromBattle } from '@showdex/utils/battle'; import { logger, printBuildInfo } from '@showdex/utils/debug'; -import { detectPlayerKeyFromBattle } from './detectPlayerKey'; import { FieldCalc } from './FieldCalc'; import { PlayerCalc } from './PlayerCalc'; import { useCalcdex } from './useCalcdex'; @@ -10,14 +10,14 @@ import styles from './Calcdex.module.scss'; interface CalcdexProps { battle?: Showdown.Battle; - tooltips?: Showdown.BattleTooltips; + // tooltips?: Showdown.BattleTooltips; } const l = logger('@showdex/pages/Calcdex/Calcdex'); export const Calcdex = ({ battle, - tooltips, + // tooltips, }: CalcdexProps): JSX.Element => { const colorScheme = useColorScheme(); @@ -31,7 +31,7 @@ export const Calcdex = ({ setAutoSelect, } = useCalcdex({ battle, - tooltips, + // tooltips, }); l.debug( @@ -51,11 +51,26 @@ export const Calcdex = ({ p2, } = state; - const playerKey = detectPlayerKeyFromBattle(battle); - const opponentKey = playerKey === 'p1' ? 'p2' : 'p1'; + // playerKey is a ref in case `battle` becomes `null` + const playerKey = React.useRef(detectPlayerKeyFromBattle(battle)); + const opponentKey = playerKey.current === 'p1' ? 'p2' : 'p1'; - const player = playerKey === 'p1' ? p1 : p2; - const opponent = playerKey === 'p1' ? p2 : p1; + React.useEffect(() => { + const detectedKey = detectPlayerKeyFromBattle(battle); + + if (!playerKey.current && detectedKey) { + playerKey.current = detectedKey; + } + }, [ + battle, + ]); + + const player = playerKey.current === 'p1' ? p1 : p2; + const opponent = playerKey.current === 'p1' ? p2 : p1; + + if (!state?.battleId) { + return null; + } return (
setSelectionIndex( - playerKey, + playerKey.current, index, )} onAutoSelectChange={(autoSelect) => setAutoSelect( - playerKey, + playerKey.current, autoSelect, )} /> @@ -104,7 +119,7 @@ export const Calcdex = ({ dex={dex} gen={gen} format={format} - playerKey={playerKey} + playerKey={opponentKey} player={opponent} opponent={player} field={field} diff --git a/src/pages/Calcdex/FieldCalc.tsx b/src/pages/Calcdex/FieldCalc.tsx index 99f694f6..e1378be7 100644 --- a/src/pages/Calcdex/FieldCalc.tsx +++ b/src/pages/Calcdex/FieldCalc.tsx @@ -5,7 +5,7 @@ import { Dropdown } from '@showdex/components/form'; import { TableGrid, TableGridItem } from '@showdex/components/layout'; import { Button } from '@showdex/components/ui'; import { TerrainNames, WeatherNames } from '@showdex/consts'; -import type { CalcdexBattleField } from './CalcdexReducer'; +import type { CalcdexBattleField } from '@showdex/redux/store'; import styles from './FieldCalc.module.scss'; interface FieldCalcProps { @@ -13,7 +13,7 @@ interface FieldCalcProps { style?: React.CSSProperties; battleId?: string; field?: CalcdexBattleField; - onFieldChange?: (field: Partial) => void; + onFieldChange?: (field: DeepPartial) => void; } export const FieldCalc = ({ diff --git a/src/pages/Calcdex/PlayerCalc.tsx b/src/pages/Calcdex/PlayerCalc.tsx index 375fc22d..b8b3cce3 100644 --- a/src/pages/Calcdex/PlayerCalc.tsx +++ b/src/pages/Calcdex/PlayerCalc.tsx @@ -10,7 +10,7 @@ import type { CalcdexPlayer, CalcdexPlayerKey, CalcdexPokemon, -} from './CalcdexReducer'; +} from '@showdex/redux/store'; import { PokeCalc } from './PokeCalc'; import styles from './PlayerCalc.module.scss'; @@ -25,7 +25,7 @@ interface PlayerCalcProps { opponent: CalcdexPlayer; field?: CalcdexBattleField; defaultName?: string; - onPokemonChange?: (pokemon: Partial) => void; + onPokemonChange?: (playerKey: CalcdexPlayerKey, pokemon: DeepPartial) => void; onIndexSelect?: (index: number) => void; onAutoSelectChange?: (autoSelect: boolean) => void; } @@ -99,7 +99,7 @@ export const PlayerCalc = ({ !autoSelect && styles.inactive, )} label="Auto" - tooltip={`${autoSelect ? 'Manually ' : 'Auto-'}Select Active Pokémon`} + tooltip={`${autoSelect ? 'Manually ' : 'Auto-'}Select Pokémon`} absoluteHover disabled={!pokemon?.length} onPress={() => onAutoSelectChange?.(!autoSelect)} @@ -110,10 +110,10 @@ export const PlayerCalc = ({ {' '}•{' '} - ELO{' '} + {rating}{' '} - {rating} + ELO }
@@ -165,7 +165,7 @@ export const PlayerCalc = ({ attackerSide: playerSideId === playerKey ? field?.attackerSide : field?.defenderSide, defenderSide: playerSideId === playerKey ? field?.defenderSide : field?.attackerSide, }} - onPokemonChange={onPokemonChange} + onPokemonChange={(p) => onPokemonChange?.(playerKey, p)} />
); diff --git a/src/pages/Calcdex/PokeCalc.tsx b/src/pages/Calcdex/PokeCalc.tsx index 2958570d..cb023b85 100644 --- a/src/pages/Calcdex/PokeCalc.tsx +++ b/src/pages/Calcdex/PokeCalc.tsx @@ -3,9 +3,7 @@ import cx from 'classnames'; // import { logger } from '@showdex/utils/debug'; import type { Generation } from '@pkmn/data'; import type { GenerationNum } from '@pkmn/types'; -import type { CalcdexBattleField, CalcdexPokemon } from './CalcdexReducer'; -import { createSmogonField } from './createSmogonField'; -import { createSmogonPokemon } from './createSmogonPokemon'; +import type { CalcdexBattleField, CalcdexPokemon } from '@showdex/redux/store'; import { PokeInfo } from './PokeInfo'; import { PokeMoves } from './PokeMoves'; import { PokeStats } from './PokeStats'; @@ -21,7 +19,7 @@ interface PokeCalcProps { playerPokemon: CalcdexPokemon; opponentPokemon: CalcdexPokemon; field?: CalcdexBattleField; - onPokemonChange?: (pokemon: Partial) => void; + onPokemonChange?: (pokemon: DeepPartial) => void; } // const l = logger('@showdex/pages/Calcdex/PokeCalc'); @@ -37,43 +35,61 @@ export const PokeCalc = ({ field, onPokemonChange, }: PokeCalcProps): JSX.Element => { - const smogonPlayerPokemon = createSmogonPokemon(gen, dex, playerPokemon); - const smogonOpponentPokemon = createSmogonPokemon(gen, dex, opponentPokemon); - const smogonField = createSmogonField(field); - const calculateMatchup = useSmogonMatchup( - gen, - smogonPlayerPokemon, - smogonOpponentPokemon, - smogonField, + dex, + playerPokemon, + opponentPokemon, + field, ); const handlePokemonChange = ( mutation: DeepPartial, - ) => onPokemonChange?.({ - ...mutation, - - calcdexId: playerPokemon?.calcdexId, - ident: playerPokemon?.ident, - boosts: playerPokemon?.boosts, - - nature: mutation?.nature ?? playerPokemon?.nature, - - ivs: { - ...playerPokemon?.ivs, - ...mutation?.ivs, - }, - - evs: { - ...playerPokemon?.evs, - ...mutation?.evs, - }, - - dirtyBoosts: { - ...playerPokemon?.dirtyBoosts, - ...mutation?.dirtyBoosts, - }, - }); + ) => { + const payload: DeepPartial = { + ...mutation, + + calcdexId: playerPokemon?.calcdexId, + // ident: playerPokemon?.ident, + // boosts: playerPokemon?.boosts, + + // nature: mutation?.nature ?? playerPokemon?.nature, + + ivs: { + ...playerPokemon?.ivs, + ...mutation?.ivs, + }, + + evs: { + ...playerPokemon?.evs, + ...mutation?.evs, + }, + + dirtyBoosts: { + ...playerPokemon?.dirtyBoosts, + ...mutation?.dirtyBoosts, + }, + }; + + // clear any dirtyBoosts that match the current boosts + Object.entries(playerPokemon.boosts).forEach(([ + stat, + boost, + ]: [ + stat: Showdown.StatNameNoHp, + boost: number, + ]) => { + const dirtyBoost = payload.dirtyBoosts[stat]; + + const validBoost = typeof boost === 'number'; + const validDirtyBoost = typeof dirtyBoost === 'number'; + + if (validBoost && validDirtyBoost && dirtyBoost === boost) { + payload.dirtyBoosts[stat] = undefined; + } + }); + + onPokemonChange?.(payload); + }; return (
) => void; + onPokemonChange?: (pokemon: DeepPartial) => void; } export const PokeMoves = ({ @@ -103,7 +102,7 @@ export const PokeMoves = ({ - %KO + KO % {/* (actual) moves */} @@ -127,13 +126,12 @@ export const PokeMoves = ({ // const maxPp = move?.noPPBoosts ? (move?.pp || 0) : Math.floor((move?.pp || 0) * (8 / 5)); // const remainingPp = Math.max(maxPp - (ppUsed || maxPp), 0); - const calculatorMove = createSmogonMove( - gen, - pokemon, - moveName, - ); - - const result = calculateMatchup?.(calculatorMove); + const { + move: calculatorMove, + damageRange, + koChance, + koColor, + } = calculateMatchup?.(moveName) || {}; // Z/Max/G-Max moves bypass the original move's accuracy // (only time these moves can "miss" is if the opposing Pokemon is in a semi-vulnerable state, @@ -239,20 +237,20 @@ export const PokeMoves = ({ {/* XXX.X% – XXX.X% */} - {result?.damageRange} + {damageRange} {/* XXX% XHKO */} - {result?.koChance} + {koChance} ); diff --git a/src/pages/Calcdex/PokeStats.tsx b/src/pages/Calcdex/PokeStats.tsx index 3cfa7905..b2bacc52 100644 --- a/src/pages/Calcdex/PokeStats.tsx +++ b/src/pages/Calcdex/PokeStats.tsx @@ -9,11 +9,10 @@ import { PokemonNatureBoosts, PokemonStatNames, } from '@showdex/consts'; +import { detectStatBoostDelta, formatStatBoost } from '@showdex/utils/battle'; +import { calcPokemonStats } from '@showdex/utils/calc'; import type { Generation } from '@pkmn/data'; -import type { CalcdexPokemon } from './CalcdexReducer'; -import { calcPokemonStats } from './calcPokemonStats'; -import { detectStatBoostDelta } from './detectStatBoostDelta'; -import { formatStatBoost } from './formatStatBoost'; +import type { CalcdexPokemon } from '@showdex/redux/store'; import styles from './PokeStats.module.scss'; export interface PokeStatsProps { @@ -21,7 +20,7 @@ export interface PokeStatsProps { style?: React.CSSProperties; dex: Generation; pokemon: CalcdexPokemon; - onPokemonChange?: (pokemon: Partial) => void; + onPokemonChange?: (pokemon: DeepPartial) => void; } export const PokeStats = ({ From 4bf0b5806491314d5c9a3070fd804821e4ccccc4 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Fri, 12 Aug 2022 23:17:07 -0700 Subject: [PATCH 022/143] feat(calcdex): did the thing in the last commit & grouped presets --- src/pages/Calcdex/PokeInfo.tsx | 135 ++++++++++++++++++++++++++------- 1 file changed, 109 insertions(+), 26 deletions(-) diff --git a/src/pages/Calcdex/PokeInfo.tsx b/src/pages/Calcdex/PokeInfo.tsx index 37a0a4a2..dfa2e555 100644 --- a/src/pages/Calcdex/PokeInfo.tsx +++ b/src/pages/Calcdex/PokeInfo.tsx @@ -10,16 +10,18 @@ import { import { Dropdown } from '@showdex/components/form'; import { Button } from '@showdex/components/ui'; import { + FormatLabels, PokemonCommonNatures, PokemonNatureBoosts, PokemonToggleAbilities, } from '@showdex/consts'; import { openSmogonUniversity } from '@showdex/utils/app'; +import { detectToggledAbility } from '@showdex/utils/battle'; +import { calcPokemonHp } from '@showdex/utils/calc'; import type { AbilityName, ItemName } from '@pkmn/data'; import type { GenerationNum } from '@pkmn/types'; -import type { CalcdexPokemon } from './CalcdexReducer'; -import { calcPokemonHp } from './calcPokemonHp'; -import { detectToggledAbility } from './detectToggledAbility'; +import type { CalcdexPokemon, CalcdexPokemonPreset } from '@showdex/redux/store'; +import { usePresets } from './usePresets'; import styles from './PokeInfo.module.scss'; export interface PokeInfoProps { @@ -28,7 +30,7 @@ export interface PokeInfoProps { gen?: GenerationNum; format?: string; pokemon: CalcdexPokemon; - onPokemonChange?: (pokemon: Partial) => void; + onPokemonChange?: (pokemon: DeepPartial) => void; } export const PokeInfo = ({ @@ -41,6 +43,99 @@ export const PokeInfo = ({ }: PokeInfoProps): JSX.Element => { const colorScheme = useColorScheme(); + const find = usePresets({ + format, + }); + + const downloadedSets = React.useMemo( + () => (pokemon?.speciesForme ? find(pokemon.speciesForme, true) : []), + [find, pokemon], + ); + + const presets = React.useMemo(() => [ + ...(pokemon?.presets || []), + ...downloadedSets, + ], [ + downloadedSets, + pokemon?.presets, + ]); + + const presetOptions = presets.reduce<({ label: string; options: ({ label: string; value: string; })[]; })[]>((options, preset) => { + const genlessFormat = preset.format.replace(`gen${gen}`, ''); + const groupLabel = FormatLabels?.[genlessFormat] || genlessFormat; + const group = options.find((option) => option.label === groupLabel); + + const option = { + label: preset.name, + value: preset.calcdexId, + }; + + if (!group) { + options.push({ + label: groupLabel, + options: [option], + }); + } else { + group.options.push(option); + } + + return options; + }, []); + + const applyPreset = React.useCallback((preset: CalcdexPokemonPreset) => { + const mutation: DeepPartial = { + preset: preset.calcdexId, + moves: preset.moves, + altMoves: preset.altMoves, + nature: preset.nature, + dirtyAbility: pokemon.ability !== preset.ability ? preset.ability : null, + item: !pokemon.item || pokemon.item === '(exists)' ? preset.item : pokemon.item, + dirtyItem: pokemon.item && pokemon.item !== '(exists)' && pokemon.item !== preset.item ? preset.item : null, + ivs: { ...preset.ivs }, + evs: { // not specifying the 0's may cause any unspecified EVs to remain! + hp: preset.evs?.hp || 0, + atk: preset.evs?.atk || 0, + def: preset.evs?.def || 0, + spa: preset.evs?.spa || 0, + spd: preset.evs?.spd || 0, + spe: preset.evs?.spe || 0, + }, + }; + + if (Array.isArray(preset.altAbilities)) { + mutation.altAbilities = [...preset.altAbilities]; + } + + if (Array.isArray(preset.altItems)) { + mutation.altItems = [...preset.altItems]; + } + + if (preset.format?.includes('random')) { + mutation.ability = preset.ability; + mutation.dirtyAbility = null; + + mutation.item = preset.item; + mutation.dirtyItem = null; + } + + onPokemonChange?.(mutation); + }, [ + onPokemonChange, + pokemon, + ]); + + React.useEffect(() => { + if (!pokemon?.calcdexId || !pokemon.autoPreset || pokemon.preset || !presets.length) { + return; + } + + applyPreset(presets[0]); + }, [ + applyPreset, + pokemon, + presets, + ]); + const pokemonKey = pokemon?.calcdexId || pokemon?.name || '???'; const friendlyPokemonName = pokemon?.speciesForme || pokemon?.name || pokemonKey; @@ -76,7 +171,7 @@ export const PokeInfo = ({
-
+
{pokemon?.name || '--'} @@ -108,7 +203,7 @@ export const PokeInfo = ({
-
+
{/* HP{' '} */} @@ -171,35 +266,23 @@ export const PokeInfo = ({ name: `PokeInfo:Preset:${pokemonKey}`, value: pokemon?.preset, onChange: (calcdexId: string) => { - const preset = pokemon.presets - .find((p) => p?.calcdexId === calcdexId); + const preset = presets.find((p) => p?.calcdexId === calcdexId); if (!preset) { return; } - onPokemonChange?.({ - preset: calcdexId, - ivs: preset.ivs, - evs: preset.evs, - moves: preset.moves, - altMoves: preset.altMoves, - nature: preset.nature, - dirtyAbility: pokemon.ability !== preset.ability ? preset.ability : null, - altAbilities: preset.altAbilities, - altItems: preset.altItems, - item: !pokemon.item || pokemon.item === '(exists)' ? preset.item : pokemon.item, - dirtyItem: pokemon.item && pokemon.item !== '(exists)' && pokemon.item !== preset.item ? preset.item : null, - }); + applyPreset(preset); }, }} - options={pokemon?.presets?.filter((p) => p?.calcdexId).map((p) => ({ - label: p?.name, - value: p?.calcdexId, - }))} + // options={presets.filter((p) => p?.calcdexId).map((p) => ({ + // label: p.name, + // value: p.calcdexId, + // }))} + options={presetOptions} noOptionsMessage="No Sets" clearable={false} - disabled={!pokemon?.speciesForme || !pokemon?.presets?.length} + disabled={!pokemon?.speciesForme || !presets.length} />
From deb751bca924a58a48d8e918f7651d196d721bd6 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Fri, 12 Aug 2022 23:18:01 -0700 Subject: [PATCH 023/143] feat(calcdex): removed hardcoded max-widths in PokeInfo styling now they're hardcoded percentages that I arbitrarily chose using the element inspector lmao --- src/pages/Calcdex/PokeInfo.module.scss | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/pages/Calcdex/PokeInfo.module.scss b/src/pages/Calcdex/PokeInfo.module.scss index e2b594c2..678b86f5 100644 --- a/src/pages/Calcdex/PokeInfo.module.scss +++ b/src/pages/Calcdex/PokeInfo.module.scss @@ -47,6 +47,15 @@ } } +.infoContainer { + flex: 1.25; +} + +.firstLine { + max-width: 100%; + @include spacing.margin($bottom: 1px); +} + .piconContainer { flex: 0 0 40px; transform: translateY(-2px); @@ -55,7 +64,7 @@ .pokemonName { display: inline-block; vertical-align: top; - max-width: 66px; + max-width: 45%; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; @@ -71,10 +80,6 @@ } } -.infoContainer { - flex: 1.25; -} - .toggleButtonLabel { .light &.inactive { color: colors.$black; @@ -94,6 +99,11 @@ } } +.secondLine { + min-height: 15px; + user-select: none; +} + .hpBar { @include spacing.margin($bottom: 2px); } From abe09a71e9d6689fda9179e0786a152f3e226655 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Fri, 12 Aug 2022 23:18:24 -0700 Subject: [PATCH 024/143] feat(styles): added showdown background colors --- src/styles/config/_colors.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/styles/config/_colors.scss b/src/styles/config/_colors.scss index 6ee76274..6ac2e64c 100644 --- a/src/styles/config/_colors.scss +++ b/src/styles/config/_colors.scss @@ -8,3 +8,6 @@ $blue: #2196F3; // MD Blue 500 $transparent-black: rgba(0, 0, 0, 0); $transparent-white: rgba(255, 255, 255, 0); $transparent: $transparent-white; + +$showdown-background-light: #EEF2F5; +$showdown-background-dark: #444444; From 75c299a3bd72b670d116068278c0704dee08d5e1 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Fri, 12 Aug 2022 23:18:56 -0700 Subject: [PATCH 025/143] feat(calcdex): updated calcdex background colors --- src/pages/Calcdex/Calcdex.module.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/Calcdex/Calcdex.module.scss b/src/pages/Calcdex/Calcdex.module.scss index e62f1450..49f5b6e2 100644 --- a/src/pages/Calcdex/Calcdex.module.scss +++ b/src/pages/Calcdex/Calcdex.module.scss @@ -25,11 +25,11 @@ } &.light::before { - background-color: color.alpha(colors.$white, 0.4); + background-color: color.alpha(colors.$showdown-background-light, 0.4); } &.dark::before { - background-color: color.alpha(colors.$black, 0.5); + background-color: color.alpha(colors.$showdown-background-dark, 0.6); } } From 29479036689272f5f560c72a7e2cd1cefaf53f30 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Fri, 12 Aug 2022 23:19:20 -0700 Subject: [PATCH 026/143] feat(consts): removed abbreviations in formats --- src/consts/formats.ts | 196 ++++++++++++++++++++---------------------- 1 file changed, 94 insertions(+), 102 deletions(-) diff --git a/src/consts/formats.ts b/src/consts/formats.ts index fddd5060..7ee18610 100644 --- a/src/consts/formats.ts +++ b/src/consts/formats.ts @@ -1,120 +1,112 @@ /** - * Abbreviations for Showdown battle formats. + * Labels for rendering group labels in the Pokemon presets dropdown. * - * * Typically only used for distinguishing presets by format. - * * Attempted to standardize the format names with the following rules: - * - Format should not include the generation prefix, e.g., `'gen8'`. - * - We're only interested in the format after the prefix, e.g., the `'ou'` in `'gen8ou'`. - * - Sub-formats, if any, are separated by hyphens (`-`). - * - Primary format should precede sub-formats, e.g., `'gen8bdspou'` should be `'OU-BDSP'` and not `'BDSP-OU'`. - * - Seems counterintuitive, but makes it easier to parse through the list of presets, if sorted lexicographically. - * - Each abbreviation, whether the primary or sub-format, should be capitalized and not exceed more than 4 characters. - * - Only exception is `'XvX'` formats (where `X` is a number), where the `'v'` is lowercased cause aesthetic. - * * Still a work-in-progress cause I'm unsure how intuitive some of these formats are, like `'AAA'` and `'INHT'`. + * * Used to be all abbreviations, but since v0.1.3, these are now spelled out + * for less-common formats, like `'AG'` (Anything Goes) and `'BH'` (Balanced Hackmons). * * @since 0.1.0 */ export const FormatLabels: Record = { '1v1': '1v1', - '2v2doubles': '2v2-2X', - almostanyability: 'AAA', - alternatium: 'ALT', - anythinggoes: 'AG', - balancedhackmons: 'BH', - battlefactory: 'BF', - battlespotsingles: 'BSP-1X', - battlespotdoubles: 'BSP-2X', - battlestadiumsingles: 'BST-1X', - battlestadiumdoubles: 'BST-2X', - bdsp3v3singles: '3v3-1X-BDSP', - bdspbattlefestivaldoubles: 'BF-2X-BDSP', - bdspcap: 'CAP-BDSP', - bdspmonotype: 'MONO-BDSP', - bdspnu: 'NU-BDSP', - bdspou: 'OU-BDSP', // BrilliantDiamondShiningPearl - bdsppurehackmons: 'PH-BDSP', - bdsprandombattle: 'RNG-BDSP', - bdspru: 'RU-BDSP', - bdspubers: 'UBER-BDSP', - bdspuu: 'UU-BDSP', - bssfactory: 'BSSF', - camomons: 'CAMO', + '2v2doubles': '2v2 Doubles', + almostanyability: 'Almost Any Ability', + alternatium: 'Alternatium', + anythinggoes: 'Anything Goes', + balancedhackmons: 'Balanced Hackmons', + battlefactory: 'Battle Factory', + battlespotsingles: 'Battle Spot Singles', + battlespotdoubles: 'Battle Spot Doubles', + battlestadiumsingles: 'Battle Stadium Singles', + battlestadiumdoubles: 'Battle Stadium Doubles', + bdsp3v3singles: '3v3 BDSP Singles', + bdspbattlefestivaldoubles: 'Battle Fest BDSP Doubles', + bdspcap: 'CAP BDSP', + bdspmonotype: 'Monotype BDSP', + bdspnu: 'NU BDSP', + bdspou: 'OU BDSP', // BrilliantDiamondShiningPearl + bdsppurehackmons: 'Pure Hackmons BDSP', + bdsprandombattle: 'Randoms BDSP', + bdspru: 'RU BDSP', + bdspubers: 'Ubers BDSP', + bdspuu: 'UU BDSP', + bssfactory: 'BSS Factory', + camomons: 'Camomons', cap: 'CAP', // CreateAPokemon (no cap, always factual) - cap1v1: 'CAP-1v1', - caplc: 'CAP-LC', - challengecup: 'CC', - challengecup1v1: 'CC-1v1', - challengecup2v2: 'CC-2v2', - crossevolution: 'XEVO', - customgame: 'CG', - doublescustomgame: 'CG-2X', - doubleshackmonscup: 'HC-2X', - doubleslc: 'LC-2X', - doublesou: 'OU-2X', - doublesubers: 'UBER-2X', - doublesuu: 'UU-2X', + cap1v1: 'CAP 1v1', + caplc: 'CAP LC', + challengecup: 'Challenge Cup', + challengecup1v1: 'Challenge Cup 1v1', + challengecup2v2: 'Challenge Cup 2v2', + crossevolution: 'Cross Evolution', + customgame: 'Customs', + doublescustomgame: 'Customs Doubles', + doubleshackmonscup: 'Hackmons Cup Doubles', + doubleslc: 'LC Doubles', + doublesou: 'OU Doubles', + doublesubers: 'Ubers Doubles', + doublesuu: 'UU Doubles', freeforall: 'FFA', - freeforallrandombattle: 'RNG-FFA', - gbusingles: 'GBU-1X', - godlygift: 'GG', - hackmonscup: 'HC', - inheritance: 'INHT', - japaneseou: 'OU-JP', + freeforallrandombattle: 'Randoms FFA', + gbusingles: 'GBU Singles', + godlygift: 'Godly Gift', + hackmonscup: 'Hackmons Cup', + inheritance: 'Inheritance', + japaneseou: 'OU Japanese', lc: 'LC', // LittleCup - lcuu: 'LC-UU', - letsgoou: 'OU-LGPE', // LetsGoPikachuEevee - linked: 'LINK', - metronomebattle: 'MTRN', - mixandmega: 'M&M', - monotype: 'MONO', - monotypebattlefactory: 'BF-MONO', - monotyperandombattle: 'RNG-MONO', - multibility: 'MBIL', - multirandombattle: 'RNG-MULT', - nationaldex: 'NDEX', - nationaldexag: 'NDEX-AG', - nationaldexmonotype: 'NDEX-MONO', - natureswap: 'NS', - nextou: 'OU-NEXT', + lcuu: 'UU LC', + letsgoou: 'OU LGPE', // LetsGoPikachuEevee + linked: 'Linked', + metronomebattle: 'Metronome', + mixandmega: 'Mix & Mega', + monotype: 'Monotype', + monotypebattlefactory: 'Battle Factory Monotype', + monotyperandombattle: 'Randoms Monotype', + multibility: 'Multibility', + multirandombattle: 'Randoms Multibility', + nationaldex: 'National Dex', + nationaldexag: 'National Dex AG', + nationaldexmonotype: 'National Dex Monotype', + natureswap: 'Nature Swap', + nextou: 'OU Next', nfe: 'NFE', // NotFullyEvolved - nintendocup1997: 'NC-97', - nintendocup2000: 'NC-00', + nintendocup1997: 'Nintendo Cup 1997', + nintendocup2000: 'Nintendo Cup 2000', nu: 'NU', // NeverUsed ou: 'OU', // OverUsed - oublitz: 'OU-BZ', // went w/ BZ for Blitz cause BL = BanList! (like in PUBL, UUBL, etc.) - pokebilities: 'PBIL', + oublitz: 'OU Blitz', // went w/ BZ for Blitz cause BL = BanList! (like in PUBL, UUBL, etc.) + pokebilities: 'Pokebilities', pu: 'PU', // PU (as in, "P-U, smells like ass"... I think) - purehackmons: 'PH', - randombattle: 'RNG', // thought about RB, but that sounds like Arby's and not Random Battles lmao - randombattleblitz: 'RNG-BZ', - randombattlemayhem: 'RNG-MH', - randombattlenodmax: 'RNG-NODM', - randomdex: 'RNG-DEX', - randomdoublesbattle: 'RNG-2X', + purehackmons: 'Pure Hackmons', + randombattle: 'Randoms', + randombattleblitz: 'Randoms Blitz', + randombattlemayhem: 'Randoms Mayhem', + randombattlenodmax: 'Randoms No-Dmax', + randomdex: 'Randoms Dex', + randomdoublesbattle: 'Randoms Doubles', ru: 'RU', // RarelyUsed - sharedpower: 'SPWR', - stabmons: 'STAB', // SameTypeAttackBonus - stadiumou: 'OU-ST', - superstaffbros4: 'SSB-4', - thelosersgame: 'TLG', - tradebacksou: 'OU-TB', - triplescustomgame: 'CG-3X', - ubers: 'UBER', - unratedrandombattle: 'RNG-UR', + sharedpower: 'Shared Power', + stabmons: 'Stabmons', // SameTypeAttackBonus + stadiumou: 'OU Stadium', + superstaffbros4: 'Super Staff Bros 4', + thelosersgame: 'The Loser Game', + tradebacksou: 'OU Tradebacks', + triplescustomgame: 'Customs Triples', + ubers: 'Ubers', + unratedrandombattle: 'Randoms Unrated', uu: 'UU', // UnderUsed - vgc2009: 'VGC-09', // VideoGameChampionships - vgc2010: 'VGC-10', - vgc2011: 'VGC-11', - vgc2012: 'VGC-12', - vgc2013: 'VGC-13', - vgc2014: 'VGC-14', - vgc2015: 'VGC-15', - vgc2016: 'VGC-16', - vgc2017: 'VGC-17', - vgc2018: 'VGC-18', - vgc2019: 'VGC-19', - vgc2020: 'VGC-20', - vgc2021: 'VGC-21', - vgc2022: 'VGC-22', + vgc2009: 'VGC 2009', // VideoGameChampionships + vgc2010: 'VGC 2010', + vgc2011: 'VGC 2011', + vgc2012: 'VGC 2012', + vgc2013: 'VGC 2013', + vgc2014: 'VGC 2014', + vgc2015: 'VGC 2015', + vgc2016: 'VGC 2016', + vgc2017: 'VGC 2017', + vgc2018: 'VGC 2018', + vgc2019: 'VGC 2019', + vgc2020: 'VGC 2020', + vgc2021: 'VGC 2021', + vgc2022: 'VGC 2022', zu: 'ZU', // ZeroUsed }; From 31ad180749d90717d6cc2b49a03357a91b58920e Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Fri, 12 Aug 2022 23:19:52 -0700 Subject: [PATCH 027/143] chore: updated host_permissions url in manifest --- src/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/manifest.json b/src/manifest.json index 3061041e..641101d9 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -10,7 +10,7 @@ ], "host_permissions": [ - "https://data.pkmn.cc/*" + "https://pkmn.github.io/*" ], "background": { From 24db9a93e73ad96402296431df477f1652fd3e16 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Sun, 14 Aug 2022 23:51:19 -0700 Subject: [PATCH 028/143] fix(redux): swapped to using ident over searchid in syncBattle --- src/redux/actions/syncBattle.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/redux/actions/syncBattle.ts b/src/redux/actions/syncBattle.ts index 45abd218..3fbb36a9 100644 --- a/src/redux/actions/syncBattle.ts +++ b/src/redux/actions/syncBattle.ts @@ -148,17 +148,17 @@ export const syncBattle = createAsyncThunk p.searchid); + playerState.pokemonOrder = myPokemon.map((p) => p.ident); } // reconstruct a full list of the current player's Pokemon, whether revealed or not // (but if we don't have the relevant info [i.e., !isMyPokemonSide], then just access the player's `pokemon`) - const playerPokemon = isMyPokemonSide ? playerState.pokemonOrder.map((searchid) => { - const serverPokemon = myPokemon.find((p) => p.searchid === searchid); + const playerPokemon = isMyPokemonSide ? playerState.pokemonOrder.map((ident) => { + const serverPokemon = myPokemon.find((p) => p.ident === ident); - // try to find a matching clientPokemon that has already been revealed using the searchid, + // try to find a matching clientPokemon that has already been revealed using the ident, // which is seemingly consistent between the player's `pokemon` (Pokemon[]) and `myPokemon` (ServerPokemon[]) - const clientPokemonIndex = player.pokemon.findIndex((p) => p.searchid === serverPokemon.searchid); + const clientPokemonIndex = player.pokemon.findIndex((p) => p.ident === serverPokemon.ident); if (clientPokemonIndex > -1) { return player.pokemon[clientPokemonIndex]; @@ -200,7 +200,7 @@ export const syncBattle = createAsyncThunk calcPokemonCalcdexId({ ...p, slot: j }) === clientPokemonId) : - myPokemon.find((p) => p.searchid === clientPokemon.searchid) : + myPokemon.find((p) => p.ident === clientPokemon.ident) : null; const matchedPokemonIndex = playerState.pokemon.findIndex((p) => p.calcdexId === clientPokemonId); @@ -303,10 +303,10 @@ export const syncBattle = createAsyncThunk p === player.active[0]) : - playerPokemon.findIndex((p) => p.searchid === player.active[0].searchid) : + playerPokemon.findIndex((p) => p.ident === player.active[0].ident) : -1; if (activeIndex > -1) { From 18ccf89d6d3840f85679825583daeb138b44e873 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Mon, 15 Aug 2022 04:26:11 -0700 Subject: [PATCH 029/143] fix(utils): fixed syncField not removing weather/terrain --- src/utils/battle/syncField.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/utils/battle/syncField.ts b/src/utils/battle/syncField.ts index c35999ea..a7435670 100644 --- a/src/utils/battle/syncField.ts +++ b/src/utils/battle/syncField.ts @@ -27,17 +27,17 @@ export const syncField = ( const updatedField = sanitizeField(battle, attackerIndex, defenderIndex); const fieldSideKeys = <(keyof CalcdexBattleField)[]> ['attackerSide', 'defenderSide']; - const fieldRemainingKeys = (<(keyof CalcdexBattleField)[]>Object.keys(updatedField || {})) + const fieldRemainingKeys = (<(keyof CalcdexBattleField)[]> Object.keys(updatedField || {})) .filter((key) => !fieldSideKeys.includes(key)); - let didChange = false; + // let didChange = false; fieldRemainingKeys.forEach((key) => { const value = updatedField?.[key]; - if (!value && !['string', 'number', 'boolean'].includes(typeof value)) { - return; - } + // if (!value && !['string', 'number', 'boolean'].includes(typeof value)) { + // return; + // } const originalValue = field?.[key]; @@ -47,9 +47,9 @@ export const syncField = ( (> newField)[key] = value; - if (!didChange) { - didChange = true; - } + // if (!didChange) { + // didChange = true; + // } }); fieldSideKeys.forEach((sideKey) => { @@ -69,15 +69,15 @@ export const syncField = ( (> newField[sideKey])[key] = value; - if (!didChange) { - didChange = true; - } + // if (!didChange) { + // didChange = true; + // } }); }); - if (!didChange) { - return field; - } + // if (!didChange) { + // return field; + // } return newField; }; From 06e792ab61caaf46478bb487d9f356830a75b570 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Mon, 15 Aug 2022 04:27:01 -0700 Subject: [PATCH 030/143] refac(utils): renamed calcPokemonFinalStats --- src/utils/calc/calcPokemonFinalStats.ts | 239 ++++++++++++++++++++++++ src/utils/calc/index.ts | 1 + 2 files changed, 240 insertions(+) create mode 100644 src/utils/calc/calcPokemonFinalStats.ts diff --git a/src/utils/calc/calcPokemonFinalStats.ts b/src/utils/calc/calcPokemonFinalStats.ts new file mode 100644 index 00000000..1a06f99f --- /dev/null +++ b/src/utils/calc/calcPokemonFinalStats.ts @@ -0,0 +1,239 @@ +import { getFinalSpeed, getModifiedStat } from '@smogon/calc/dist/mechanics/util'; +import { PokemonStatNames } from '@showdex/consts'; +// import { detectSpeciesForme } from '@showdex/utils/battle'; +// import { logger } from '@showdex/utils/debug'; +import type { Generation } from '@pkmn/data'; +import type { CalcdexBattleField, CalcdexPokemon } from '@showdex/redux/store'; +// import { calcPokemonHp } from './calcPokemonHp'; +import { createSmogonField } from './createSmogonField'; +import { createSmogonPokemon } from './createSmogonPokemon'; + +// const l = logger('@showdex/pages/Calcdex/calcPokemonStats'); + +const initialStats: Showdown.StatsTable = { + hp: 0, + atk: 0, + def: 0, + spa: 0, + spd: 0, + spe: 0, +}; + +/** + * @todo Flower Gift abililty (factor in dmax). + * @todo Fur Coat ability. + * @todo Choice Band/Scarf/Specs (factor in dmax). + * @todo Gorilla Tactics ability (factor in dmax). + * @todo Grass Pelt ability. + * @todo Huge Power ability. + * @todo Hustle ability. + * @todo Marvel Scale ability. + * @since 0.1.3 + */ +export const calcPokemonFinalStats = ( + dex: Generation, + pokemon: DeepPartial, + field: CalcdexBattleField, + side: 'attacker' | 'defender', +): Showdown.StatsTable => { + // if (typeof dex?.stats?.calc !== 'function' || typeof dex?.species?.get !== 'function') { + // l.warn( + // 'cannot calculate stats since dex.stats.calc() and/or dex.species.get() are not available', + // '\n', 'dex', dex, + // '\n', 'pokemon', pokemon, + // ); + // + // return initialStats; + // } + + // const speciesForme = detectSpeciesForme(pokemon); + + // if (!speciesForme) { + // l.warn( + // 'cannot calculate stats since the Pokemon\'s detected speciesForme is falsy', + // // '\n', 'pokemon.ident', pokemon?.ident, + // '\n', 'speciesForme', speciesForme, + // '\n', 'pokemon', pokemon, + // ); + // + // return initialStats; + // } + + // const species = dex.species.get(speciesForme); + + // const nature = pokemon?.nature && typeof dex.natures?.get === 'function' ? + // dex.natures.get(pokemon.nature) : + // undefined; + + // these are used for determining stat increases/decreases due to status conditions + // const gen = dex.num; + // const hasGuts = pokemon?.ability?.toLowerCase?.() === 'guts'; + // const hasQuickFeet = pokemon?.ability?.toLowerCase?.() === 'quick feet'; + // const hasDefeatist = pokemon?.ability?.toLowerCase() === 'defeatist'; + + const smogonPokemon = createSmogonPokemon(dex, pokemon); + const smogonField = createSmogonField(field); + + // rebuild the Pokemon's base stats to make sure all values are available + // const baseStats: CalcdexPokemon['baseStats'] = { + // hp: pokemon?.baseStats?.hp ?? 0, + // atk: pokemon?.baseStats?.atk ?? 0, + // def: pokemon?.baseStats?.def ?? 0, + // spa: pokemon?.baseStats?.spa ?? 0, + // spd: pokemon?.baseStats?.spd ?? 0, + // spe: pokemon?.baseStats?.spe ?? 0, + // }; + + // const baseStats: CalcdexPokemon['baseStats'] = { + // hp: species?.baseStats?.hp ?? 0, + // atk: species?.baseStats?.atk ?? 0, + // def: species?.baseStats?.def ?? 0, + // spa: species?.baseStats?.spa ?? 0, + // spd: species?.baseStats?.spd ?? 0, + // spe: species?.baseStats?.spe ?? 0, + // }; + + // do the same thing for the Pokemon's IVs and EVs + // const ivs: CalcdexPokemon['ivs'] = { + // hp: pokemon?.ivs?.hp ?? 31, + // atk: pokemon?.ivs?.atk ?? 31, + // def: pokemon?.ivs?.def ?? 31, + // spa: pokemon?.ivs?.spa ?? 31, + // spd: pokemon?.ivs?.spd ?? 31, + // spe: pokemon?.ivs?.spe ?? 31, + // }; + + // const evs: CalcdexPokemon['evs'] = { + // hp: pokemon?.evs?.hp ?? 0, + // atk: pokemon?.evs?.atk ?? 0, + // def: pokemon?.evs?.def ?? 0, + // spa: pokemon?.evs?.spa ?? 0, + // spd: pokemon?.evs?.spd ?? 0, + // spe: pokemon?.evs?.spe ?? 0, + // }; + + // const boosts: CalcdexPokemon['boosts'] = { + // atk: (pokemon?.dirtyBoosts?.atk ?? pokemon?.boosts?.atk) || 0, + // def: (pokemon?.dirtyBoosts?.def ?? pokemon?.boosts?.def) || 0, + // spa: (pokemon?.dirtyBoosts?.spa ?? pokemon?.boosts?.spa) || 0, + // spd: (pokemon?.dirtyBoosts?.spd ?? pokemon?.boosts?.spd) || 0, + // spe: (pokemon?.dirtyBoosts?.spe ?? pokemon?.boosts?.spe) || 0, + // }; + + const calculatedStats: CalcdexPokemon['calculatedStats'] = PokemonStatNames.reduce((prev, stat) => { + if (stat === 'spe') { + const fieldSide = side === 'attacker' ? smogonField.attackerSide : smogonField.defenderSide; + + prev.spe = getFinalSpeed( + dex, + smogonPokemon, + smogonField, + fieldSide, + ); + } else { + prev[stat] = getModifiedStat( + smogonPokemon.rawStats[stat], + smogonPokemon.boosts[stat], + dex, + ); + } + + return prev; + }, { ...initialStats }); + + // const calculatedStats: CalcdexPokemon['calculatedStats'] = PokemonStatNames.reduce((prev, stat) => { + // prev[stat] = dex.stats.calc( + // stat, + // baseStats[stat], + // ivs[stat], + // evs[stat], + // pokemon?.level || 100, + // nature, + // ); + // + // // if the Pokemon is Zygarde-Complete, double its HP + // if (speciesForme === 'Zygarde-Complete' && stat === 'hp') { + // prev[stat] *= 2; + // } + // + // // re-calculate any boosted stat (except for HP, obviously) + // if (stat !== 'hp' && stat in boosts) { + // const stage = boosts[stat]; + // + // if (stage) { + // const clampedStage = Math.min(Math.max(stage, -6), 6); // -6 <= stage <= 6 + // const multiplier = ((Math.abs(clampedStage) + 2) / 2) ** (clampedStage < 0 ? -1 : 1); + // + // prev[stat] *= multiplier; + // } + // + // // handle reductions due to abilities + // if ('slowstart' in (pokemon?.volatiles || {}) && pokemon?.abilityToggled) { + // // 50% ATK/SPE reduction due to "Slow Start" + // if (['atk', 'spe'].includes(stat)) { + // prev[stat] *= 0.5; + // } + // } + // + // // handle boosts/reductions by the Pokemon's current HP + // const hp = calcPokemonHp(pokemon); + // + // if (hasDefeatist && hp <= 0.5) { + // if (['atk', 'spa'].includes(stat)) { + // prev[stat] *= 0.5; + // } + // } + // + // // handle boosts/reductions by the Pokemon's current status condition, if any + // if (pokemon?.status) { + // if (hasGuts) { + // // 50% ATK boost w/ non-volatile status condition due to "Guts" + // if (stat === 'atk') { + // prev[stat] *= 1.5; + // } + // } else if (hasQuickFeet) { + // // 50% SPE boost w/ non-volatile status condition due to "Quick Feet" + // if (stat === 'spe') { + // prev[stat] *= 1.5; + // } + // } else { // Pokemon does not have either "Guts" or "Quick Feet" + // switch (pokemon.status) { + // case 'brn': { + // // 50% ATK reduction (all gens... probably) + // if (stat === 'atk') { + // prev[stat] *= 0.5; + // } + // + // break; + // } + // + // case 'par': { + // // 25% SPE reduction if gen < 7 (i.e., gens 1 to 6), otherwise 50% SPE reduction + // if (stat === 'spe') { + // prev[stat] *= 1 - (gen < 7 ? 0.25 : 0.5); + // } + // + // break; + // } + // + // default: { + // break; + // } + // } + // } + // } + // } + // + // return prev; + // }, { ...initialStats }); + + // l.debug( + // 'calcPokemonStats() -> return calculatedStats', + // '\n', 'stats calculated for Pokemon', pokemon.ident || pokemon.speciesForme, + // '\n', 'calculatedStats', calculatedStats, + // '\n', 'pokemon', pokemon, + // '\n', 'speciesForme', speciesForme, + // ); + + return calculatedStats; +}; diff --git a/src/utils/calc/index.ts b/src/utils/calc/index.ts index 8ce7ecbb..c24bf2a1 100644 --- a/src/utils/calc/index.ts +++ b/src/utils/calc/index.ts @@ -1,5 +1,6 @@ export * from './calcCalcdexId'; export * from './calcCalcdexNonce'; +export * from './calcPokemonFinalStats'; export * from './calcPokemonHp'; export * from './calcPokemonStats'; export * from './calcSmogonMatchup'; From 50ec5656caa6e6c839b27ecb70ffe24f6861f1a6 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Mon, 15 Aug 2022 04:27:17 -0700 Subject: [PATCH 031/143] feat(utils): added calcPokemonStats --- src/utils/calc/calcPokemonStats.ts | 208 ++--------------------------- 1 file changed, 13 insertions(+), 195 deletions(-) diff --git a/src/utils/calc/calcPokemonStats.ts b/src/utils/calc/calcPokemonStats.ts index 424b6275..f58066a9 100644 --- a/src/utils/calc/calcPokemonStats.ts +++ b/src/utils/calc/calcPokemonStats.ts @@ -1,13 +1,8 @@ import { PokemonStatNames } from '@showdex/consts'; -import { detectSpeciesForme } from '@showdex/utils/battle'; -import { logger } from '@showdex/utils/debug'; import type { Generation } from '@pkmn/data'; import type { CalcdexPokemon } from '@showdex/redux/store'; -import { calcPokemonHp } from './calcPokemonHp'; -const l = logger('@showdex/pages/Calcdex/calcPokemonStats'); - -const initialStats: CalcdexPokemon['calculatedStats'] = { +const initialStats: Showdown.StatsTable = { hp: 0, atk: 0, def: 0, @@ -16,195 +11,18 @@ const initialStats: CalcdexPokemon['calculatedStats'] = { spe: 0, }; -/** - * @todo Flower Gift abililty (factor in dmax). - * @todo Fur Coat ability. - * @todo Choice Band/Scarf/Specs (factor in dmax). - * @todo Gorilla Tactics ability (factor in dmax). - * @todo Grass Pelt ability. - * @todo Huge Power ability. - * @todo Hustle ability. - * @todo Marvel Scale ability. - * @since 0.1.0 - */ export const calcPokemonStats = ( dex: Generation, pokemon: DeepPartial, -): CalcdexPokemon['calculatedStats'] => { - if (typeof dex?.stats?.calc !== 'function' || typeof dex?.species?.get !== 'function') { - l.warn( - 'cannot calculate stats since dex.stats.calc() and/or dex.species.get() are not available', - '\n', 'dex', dex, - '\n', 'pokemon', pokemon, - ); - - return initialStats; - } - - const speciesForme = detectSpeciesForme(pokemon); - - if (!speciesForme) { - l.warn( - 'cannot calculate stats since the Pokemon\'s detected speciesForme is falsy', - // '\n', 'pokemon.ident', pokemon?.ident, - '\n', 'speciesForme', speciesForme, - '\n', 'pokemon', pokemon, - ); - - return initialStats; - } - - const species = dex.species.get(speciesForme); - - const nature = pokemon?.nature && typeof dex.natures?.get === 'function' ? - dex.natures.get(pokemon.nature) : - undefined; - - // these are used for determining stat increases/decreases due to status conditions - const gen = dex.num; - const hasGuts = pokemon?.ability?.toLowerCase?.() === 'guts'; - const hasQuickFeet = pokemon?.ability?.toLowerCase?.() === 'quick feet'; - const hasDefeatist = pokemon?.ability?.toLowerCase() === 'defeatist'; - - // rebuild the Pokemon's base stats to make sure all values are available - const baseStats: CalcdexPokemon['baseStats'] = { - hp: pokemon?.baseStats?.hp ?? species?.baseStats?.hp ?? 0, - atk: pokemon?.baseStats?.atk ?? species?.baseStats?.atk ?? 0, - def: pokemon?.baseStats?.def ?? species?.baseStats?.def ?? 0, - spa: pokemon?.baseStats?.spa ?? species?.baseStats?.spa ?? 0, - spd: pokemon?.baseStats?.spd ?? species?.baseStats?.spd ?? 0, - spe: pokemon?.baseStats?.spe ?? species?.baseStats?.spe ?? 0, - }; - - // const baseStats: CalcdexPokemon['baseStats'] = { - // hp: species?.baseStats?.hp ?? 0, - // atk: species?.baseStats?.atk ?? 0, - // def: species?.baseStats?.def ?? 0, - // spa: species?.baseStats?.spa ?? 0, - // spd: species?.baseStats?.spd ?? 0, - // spe: species?.baseStats?.spe ?? 0, - // }; - - // do the same thing for the Pokemon's IVs and EVs - const ivs: CalcdexPokemon['ivs'] = { - hp: pokemon?.ivs?.hp ?? 31, - atk: pokemon?.ivs?.atk ?? 31, - def: pokemon?.ivs?.def ?? 31, - spa: pokemon?.ivs?.spa ?? 31, - spd: pokemon?.ivs?.spd ?? 31, - spe: pokemon?.ivs?.spe ?? 31, - }; - - const evs: CalcdexPokemon['evs'] = { - hp: pokemon?.evs?.hp ?? 0, - atk: pokemon?.evs?.atk ?? 0, - def: pokemon?.evs?.def ?? 0, - spa: pokemon?.evs?.spa ?? 0, - spd: pokemon?.evs?.spd ?? 0, - spe: pokemon?.evs?.spe ?? 0, - }; - - const boosts: CalcdexPokemon['boosts'] = { - atk: (pokemon?.dirtyBoosts?.atk ?? pokemon?.boosts?.atk) || 0, - def: (pokemon?.dirtyBoosts?.def ?? pokemon?.boosts?.def) || 0, - spa: (pokemon?.dirtyBoosts?.spa ?? pokemon?.boosts?.spa) || 0, - spd: (pokemon?.dirtyBoosts?.spd ?? pokemon?.boosts?.spd) || 0, - spe: (pokemon?.dirtyBoosts?.spe ?? pokemon?.boosts?.spe) || 0, - }; - - const calculatedStats: CalcdexPokemon['calculatedStats'] = PokemonStatNames.reduce((prev, stat) => { - prev[stat] = dex.stats.calc( - stat, - baseStats[stat], - ivs[stat], - evs[stat], - pokemon?.level || 100, - nature, - ); - - // if the Pokemon is Zygarde-Complete, double its HP - if (speciesForme === 'Zygarde-Complete' && stat === 'hp') { - prev[stat] *= 2; - } - - // re-calculate any boosted stat (except for HP, obviously) - if (stat !== 'hp' && stat in boosts) { - const stage = boosts[stat]; - - if (stage) { - const clampedStage = Math.min(Math.max(stage, -6), 6); // -6 <= stage <= 6 - const multiplier = ((Math.abs(clampedStage) + 2) / 2) ** (clampedStage < 0 ? -1 : 1); - - prev[stat] *= multiplier; - } - - // handle reductions due to abilities - if ('slowstart' in (pokemon?.volatiles || {}) && pokemon?.abilityToggled) { - // 50% ATK/SPE reduction due to "Slow Start" - if (['atk', 'spe'].includes(stat)) { - prev[stat] *= 0.5; - } - } - - // handle boosts/reductions by the Pokemon's current HP - const hp = calcPokemonHp(pokemon); - - if (hasDefeatist && hp <= 0.5) { - if (['atk', 'spa'].includes(stat)) { - prev[stat] *= 0.5; - } - } - - // handle boosts/reductions by the Pokemon's current status condition, if any - if (pokemon?.status) { - if (hasGuts) { - // 50% ATK boost w/ non-volatile status condition due to "Guts" - if (stat === 'atk') { - prev[stat] *= 1.5; - } - } else if (hasQuickFeet) { - // 50% SPE boost w/ non-volatile status condition due to "Quick Feet" - if (stat === 'spe') { - prev[stat] *= 1.5; - } - } else { // Pokemon does not have either "Guts" or "Quick Feet" - switch (pokemon.status) { - case 'brn': { - // 50% ATK reduction (all gens... probably) - if (stat === 'atk') { - prev[stat] *= 0.5; - } - - break; - } - - case 'par': { - // 25% SPE reduction if gen < 7 (i.e., gens 1 to 6), otherwise 50% SPE reduction - if (stat === 'spe') { - prev[stat] *= 1 - (gen < 7 ? 0.25 : 0.5); - } - - break; - } - - default: { - break; - } - } - } - } - } - - return prev; - }, { ...initialStats }); - - // l.debug( - // 'calcPokemonStats() -> return calculatedStats', - // '\n', 'stats calculated for Pokemon', pokemon.ident || pokemon.speciesForme, - // '\n', 'calculatedStats', calculatedStats, - // '\n', 'pokemon', pokemon, - // '\n', 'speciesForme', speciesForme, - // ); - - return calculatedStats; -}; +): Partial => PokemonStatNames.reduce((prev, stat) => { + prev[stat] = dex.stats.calc( + stat, + pokemon.baseStats[stat], + pokemon.ivs?.[stat] ?? 31, + pokemon.evs?.[stat] ?? 0, + pokemon.level || 100, + dex.natures.get(pokemon.nature), + ); + + return prev; +}, { ...initialStats }); From 8f4af56c0f8e2bc7a6e5096e207a62b4e0290975 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Mon, 15 Aug 2022 04:27:38 -0700 Subject: [PATCH 032/143] feat(utils): added calculatedStats in syncPokemon --- src/utils/battle/syncPokemon.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/utils/battle/syncPokemon.ts b/src/utils/battle/syncPokemon.ts index f8c0636b..311a7383 100644 --- a/src/utils/battle/syncPokemon.ts +++ b/src/utils/battle/syncPokemon.ts @@ -1,5 +1,5 @@ import { PokemonBoostNames } from '@showdex/consts'; -import { calcPresetCalcdexId, guessServerSpread } from '@showdex/utils/calc'; +import { calcPokemonStats, calcPresetCalcdexId, guessServerSpread } from '@showdex/utils/calc'; import { logger } from '@showdex/utils/debug'; import type { // AbilityName, @@ -296,6 +296,12 @@ export const syncPokemon = ( newPokemon.moves = [...serverPreset.moves]; } + // calculate the stats with the EVs/IVs from the server preset + // (note: same thing happens in applyPreset() in PokeInfo since the EVs/IVs from the preset are now available) + if (typeof dex?.stats?.calc === 'function') { + newPokemon.calculatedStats = calcPokemonStats(dex, newPokemon); + } + serverPreset.calcdexId = calcPresetCalcdexId(serverPreset); const serverPresetIndex = newPokemon.presets.findIndex((p) => p.calcdexId === serverPreset.calcdexId); @@ -318,7 +324,7 @@ export const syncPokemon = ( // newPokemon.calcdexId = calcdexId; // } - newPokemon.calcdexNonce = sanitizedPokemon.calcdexNonce; + // newPokemon.calcdexNonce = sanitizedPokemon.calcdexNonce; return newPokemon; }; From f9cfae36af866bb0beff12c006410138c47ee362 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Mon, 15 Aug 2022 04:28:06 -0700 Subject: [PATCH 033/143] fix(utils): added default gender value in calcPokemonCalcdexId --- src/utils/calc/calcCalcdexId.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/calc/calcCalcdexId.ts b/src/utils/calc/calcCalcdexId.ts index 2336c4e1..21b4ced4 100644 --- a/src/utils/calc/calcCalcdexId.ts +++ b/src/utils/calc/calcCalcdexId.ts @@ -66,8 +66,8 @@ export const calcPokemonCalcdexId = ( pokemon?.speciesForme?.replace(/-.+$/, ''), ].filter(Boolean).join(': '), - level: String(pokemon?.level), - gender: pokemon?.gender, + level: String(pokemon?.level ?? 100), + gender: pokemon?.gender || 'N', // seems like 'N'-gendered Pokemon occasionally report back with an empty string // shiny: String(pokemon?.shiny), // bad idea, subject to change mid-battle }); From d7597767392f2bd646807885e95443a81e665d13 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Mon, 15 Aug 2022 04:29:06 -0700 Subject: [PATCH 034/143] fix(utils): fixed attacker/defender sides in calcSmogonMatchup --- src/utils/calc/calcSmogonMatchup.ts | 51 +++++++++++++++-------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/src/utils/calc/calcSmogonMatchup.ts b/src/utils/calc/calcSmogonMatchup.ts index e67ee55d..818bf11a 100644 --- a/src/utils/calc/calcSmogonMatchup.ts +++ b/src/utils/calc/calcSmogonMatchup.ts @@ -12,7 +12,11 @@ import type { // Pokemon as SmogonPokemon, Result, } from '@smogon/calc'; -import type { CalcdexBattleField, CalcdexPokemon } from '@showdex/redux/store'; +import type { + CalcdexBattleField, + CalcdexPlayerKey, + CalcdexPokemon, +} from '@showdex/redux/store'; export interface CalcdexMatchupResult { /** @@ -146,6 +150,7 @@ export const calcSmogonMatchup = ( playerPokemon: CalcdexPokemon, opponentPokemon: CalcdexPokemon, playerMove: MoveName, + playerKey?: CalcdexPlayerKey, field?: CalcdexBattleField, ): CalcdexMatchupResult => { if (!dex?.num || !playerPokemon?.speciesForme || !opponentPokemon?.speciesForme || !playerMove) { @@ -164,29 +169,27 @@ export const calcSmogonMatchup = ( return null; } - const smogonPlayerPokemon = createSmogonPokemon(dex, playerPokemon); - const smogonPlayerPokemonMove = createSmogonMove(dex.num, playerPokemon, playerMove); + const smogonPlayerPokemon = createSmogonPokemon(dex, playerPokemon, playerMove); + const smogonPlayerPokemonMove = createSmogonMove(dex, playerPokemon, playerMove); const smogonOpponentPokemon = createSmogonPokemon(dex, opponentPokemon); - const smogonField = createSmogonField(field); + + const attackerSide = playerKey === 'p1' ? field?.attackerSide : field?.defenderSide; + const defenderSide = playerKey === 'p1' ? field?.defenderSide : field?.attackerSide; + + const smogonField = createSmogonField({ + ...field, + attackerSide, + defenderSide, + }); const result = calculate( - dex.num, + dex, smogonPlayerPokemon, smogonOpponentPokemon, smogonPlayerPokemonMove, smogonField, ); - l.debug( - 'calcSmogonMatchup() <- calculate()', - '\n', 'result', result, - '\n', 'dex.num', dex.num, - '\n', 'playerPokemon', playerPokemon.name || '???', playerPokemon, - '\n', 'opponentPokemon', opponentPokemon.name || '???', opponentPokemon, - '\n', 'playerMove', playerMove || '???', - '\n', 'field', field, - ); - const matchup: CalcdexMatchupResult = { move: smogonPlayerPokemonMove, damageRange: formatDamageRange(result), @@ -194,15 +197,15 @@ export const calcSmogonMatchup = ( koColor: getKoColor(result), }; - // l.debug( - // 'calcSmogonMatchup() -> return CalcdexMatchupResult', - // '\n', 'matchup', matchup, - // '\n', 'gen', gen, - // '\n', 'playerPokemon', playerPokemon.name || '???', playerPokemon, - // '\n', 'opponentPokemon', opponentPokemon.name || '???', opponentPokemon, - // '\n', 'playerMove', playerMove.name || '???', playerMove, - // '\n', 'field', field, - // ); + l.debug( + 'Calculated damage from', playerPokemon.name, 'using', playerMove, 'against', opponentPokemon.name, + '\n', 'dex.num', dex.num, + '\n', 'matchup', matchup, + '\n', 'result', result, + '\n', 'playerPokemon', playerPokemon.name || '???', playerPokemon, + '\n', 'opponentPokemon', opponentPokemon.name || '???', opponentPokemon, + '\n', 'field', field, + ); return matchup; }; From b6d5ed6af25d985a68bfae20ace351454a666c30 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Mon, 15 Aug 2022 04:30:00 -0700 Subject: [PATCH 035/143] feat(utils): added moveName arg to createSmogonPokemon --- src/utils/calc/createSmogonPokemon.ts | 48 +++++++++++++++++++-------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/src/utils/calc/createSmogonPokemon.ts b/src/utils/calc/createSmogonPokemon.ts index 54b7f6fe..4f77465a 100644 --- a/src/utils/calc/createSmogonPokemon.ts +++ b/src/utils/calc/createSmogonPokemon.ts @@ -2,17 +2,18 @@ import { Pokemon as SmogonPokemon } from '@smogon/calc'; import { PokemonToggleAbilities } from '@showdex/consts'; import { detectPokemonIdent, detectSpeciesForme } from '@showdex/utils/battle'; import { logger } from '@showdex/utils/debug'; -import type { Generation } from '@pkmn/data'; +import type { Generation, MoveName } from '@pkmn/data'; import type { State as SmogonState } from '@smogon/calc'; import type { CalcdexPokemon } from '@showdex/redux/store'; import { calcPokemonHp } from './calcPokemonHp'; -import { calcPokemonStats } from './calcPokemonStats'; +// import { calcPokemonStats } from './calcPokemonStats'; const l = logger('@showdex/pages/Calcdex/createSmogonPokemon'); export const createSmogonPokemon = ( dex: Generation, pokemon: CalcdexPokemon, + moveName?: MoveName, ): SmogonPokemon => { if (typeof dex?.num !== 'number' || dex.num < 1) { // l.warn( @@ -38,7 +39,12 @@ export const createSmogonPokemon = ( return null; } - const speciesForme = detectSpeciesForme(pokemon); + const speciesForme = SmogonPokemon.getForme( + dex, + detectSpeciesForme(pokemon), + pokemon?.dirtyItem || pokemon?.item, + moveName, + ); // shouldn't happen, but just in case, ja feel if (!speciesForme) { @@ -53,10 +59,11 @@ export const createSmogonPokemon = ( return null; } + let ability = pokemon?.dirtyAbility ?? pokemon?.ability; + // note: Multiscale is in the PokemonToggleAbilities list, but isn't technically toggleable, per se. // we only allow it to be toggled on/off since it works like a Focus Sash (i.e., depends on the Pokemon's HP). // (to calculate() of `smogon/calc`, it'll have no idea since we'll be passing no ability if toggled off) - let ability = pokemon?.dirtyAbility ?? pokemon?.ability; const hasMultiscale = ability?.toLowerCase?.() === 'multiscale'; const toggleAbility = !hasMultiscale && PokemonToggleAbilities.includes(ability); @@ -66,9 +73,12 @@ export const createSmogonPokemon = ( const options: ConstructorParameters[2] = { ...( pokemon), + ability, abilityOn: toggleAbility ? pokemon?.abilityToggled : undefined, + item: pokemon?.dirtyItem ?? pokemon?.item, + ivs: { hp: pokemon?.ivs?.hp ?? 31, atk: pokemon?.ivs?.atk ?? 31, @@ -77,6 +87,7 @@ export const createSmogonPokemon = ( spd: pokemon?.ivs?.spd ?? 31, spe: pokemon?.ivs?.spe ?? 31, }, + evs: { hp: pokemon?.evs?.hp ?? 0, atk: pokemon?.evs?.atk ?? 0, @@ -85,6 +96,7 @@ export const createSmogonPokemon = ( spd: pokemon?.evs?.spd ?? 0, spe: pokemon?.evs?.spe ?? 0, }, + boosts: { atk: pokemon?.dirtyBoosts?.atk ?? pokemon?.boosts?.atk ?? 0, def: pokemon?.dirtyBoosts?.def ?? pokemon?.boosts?.def ?? 0, @@ -95,21 +107,29 @@ export const createSmogonPokemon = ( }; // calculate the Pokemon's current HP - const calculatedStats = calcPokemonStats(dex, pokemon); + // const calculatedStats = calcPokemonStats(dex, pokemon); - if (typeof calculatedStats?.hp === 'number') { - const currentHp = calcPokemonHp(pokemon); - const { hp: hpStat } = calculatedStats; - - // if the Pokemon fainted, assume it has full HP as to not break the damage calc - options.curHP = (currentHp || 1) * hpStat; - } + // if (typeof calculatedStats?.hp === 'number') { + // const currentHp = calcPokemonHp(pokemon); + // const { hp: hpStat } = calculatedStats; + // + // // if the Pokemon fainted, assume it has full HP as to not break the damage calc + // options.curHP = (currentHp || 1) * hpStat; + // } const smogonPokemon = new SmogonPokemon( - dex.num, - speciesForme === 'Aegislash' ? 'Aegislash-Blade' : speciesForme, // this hurts my soul + dex, + speciesForme, options, ); + if (typeof smogonPokemon?.rawStats?.hp === 'number') { + const currentHp = calcPokemonHp(pokemon); + const { hp: hpStat } = smogonPokemon.rawStats; + + // if the Pokemon fainted, assume it has full HP as to not break the damage calc + smogonPokemon.originalCurHP = (currentHp || 1) * hpStat; + } + return smogonPokemon; }; From aab7423733d296b5185c9abeec73459691ae98c4 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Mon, 15 Aug 2022 04:30:45 -0700 Subject: [PATCH 036/143] refac(utils): replaced gen arg with dex in createSmogonMove --- src/utils/calc/createSmogonMove.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/utils/calc/createSmogonMove.ts b/src/utils/calc/createSmogonMove.ts index c5e0f002..903a9f35 100644 --- a/src/utils/calc/createSmogonMove.ts +++ b/src/utils/calc/createSmogonMove.ts @@ -1,22 +1,22 @@ import { Move as SmogonMove } from '@smogon/calc'; -import type { GenerationNum, MoveName } from '@pkmn/data'; +import type { Generation, MoveName } from '@pkmn/data'; import type { CalcdexPokemon } from '@showdex/redux/store'; export const createSmogonMove = ( - gen: GenerationNum, + dex: Generation, pokemon: CalcdexPokemon, moveName: MoveName, ): SmogonMove => { - if (!gen || !pokemon?.speciesForme || !moveName) { + if (!dex?.num || !pokemon?.speciesForme || !moveName) { return null; } - const smogonMove = new SmogonMove(gen, moveName, { + const smogonMove = new SmogonMove(dex, moveName, { species: pokemon?.rawSpeciesForme || pokemon?.speciesForme, ability: pokemon?.dirtyAbility ?? pokemon?.ability, item: pokemon?.dirtyItem ?? pokemon?.item, - useZ: gen === 7 && pokemon?.useUltimateMoves, - useMax: gen === 8 && pokemon?.useUltimateMoves, + useZ: dex.num === 7 && pokemon?.useUltimateMoves, + useMax: dex.num === 8 && pokemon?.useUltimateMoves, isCrit: pokemon?.criticalHit ?? false, }); From d43ccc20ab217f7c7b75a03957a2a9c156c2f713 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Mon, 15 Aug 2022 04:31:25 -0700 Subject: [PATCH 037/143] docs(redux): updated some calcdex type comments --- src/redux/store/calcdexSlice.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/redux/store/calcdexSlice.ts b/src/redux/store/calcdexSlice.ts index 9dfd7dbb..39e3dd5d 100644 --- a/src/redux/store/calcdexSlice.ts +++ b/src/redux/store/calcdexSlice.ts @@ -407,6 +407,8 @@ export interface CalcdexPokemon extends CalcdexLeanPokemon { /** * Calculated stats of the Pokemon based on its current properties. * + * * This does not factor in items, abilities, or field conditions. + * * @default * ```ts * { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0 } @@ -528,6 +530,10 @@ export interface CalcdexPlayer extends CalcdexLeanSide { * Keeps track of the ordering of the Pokemon. * * * Each element should be some unique identifier for the Pokemon that's hopefully somewhat consistent. + * - Wouldn't recommend using `searchid` as it includes the `speciesForme`, subject to change. + * - For instance, `searchid` may read `'p1: Zygarde|Zygarde'`, but later read `'p1: Zygarde|Zygarde-Complete'`, + * which obviously doesn't qualify as "consistent." + * - `ident` seems to be the most viable property here. * * Typically should only be used for ordering `myPokemon` on initialization. * - Array ordering of `myPokemon` switches to place the last-switched in Pokemon first. * - Since `calcdexId` internally uses the `slot` value, this re-ordering mechanic produces inconsistent IDs. From dd21eac96731f69c92e1767c5e00ed25c12bc8c9 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Mon, 15 Aug 2022 04:31:51 -0700 Subject: [PATCH 038/143] feat(calcdex): added playerKey to useSmogonMatchup hook --- src/pages/Calcdex/useSmogonMatchup.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/Calcdex/useSmogonMatchup.ts b/src/pages/Calcdex/useSmogonMatchup.ts index 6fc8c815..34ddbdd4 100644 --- a/src/pages/Calcdex/useSmogonMatchup.ts +++ b/src/pages/Calcdex/useSmogonMatchup.ts @@ -6,7 +6,7 @@ import type { Generation, MoveName } from '@pkmn/data'; // Move as SmogonMove, // // Pokemon as SmogonPokemon, // } from '@smogon/calc'; -import type { CalcdexBattleField, CalcdexPokemon } from '@showdex/redux/store'; +import type { CalcdexBattleField, CalcdexPlayerKey, CalcdexPokemon } from '@showdex/redux/store'; import type { CalcdexMatchupResult } from '@showdex/utils/calc'; export type SmogonMatchupHookCalculator = ( @@ -24,6 +24,7 @@ export const useSmogonMatchup = ( dex: Generation, playerPokemon: CalcdexPokemon, opponentPokemon: CalcdexPokemon, + playerKey?: CalcdexPlayerKey, field?: CalcdexBattleField, ): SmogonMatchupHookCalculator => React.useCallback(( playerMove, @@ -32,10 +33,12 @@ export const useSmogonMatchup = ( playerPokemon, opponentPokemon, playerMove, + playerKey, field, ), [ dex, playerPokemon, opponentPokemon, + playerKey, field, ]); From 9c46791767628a02430008b12db6459599e9c4c8 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Mon, 15 Aug 2022 04:32:17 -0700 Subject: [PATCH 039/143] fix(calcdex): fixed attacker/defender sides in FieldCalc --- src/pages/Calcdex/FieldCalc.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/pages/Calcdex/FieldCalc.tsx b/src/pages/Calcdex/FieldCalc.tsx index e1378be7..9365b514 100644 --- a/src/pages/Calcdex/FieldCalc.tsx +++ b/src/pages/Calcdex/FieldCalc.tsx @@ -5,13 +5,14 @@ import { Dropdown } from '@showdex/components/form'; import { TableGrid, TableGridItem } from '@showdex/components/layout'; import { Button } from '@showdex/components/ui'; import { TerrainNames, WeatherNames } from '@showdex/consts'; -import type { CalcdexBattleField } from '@showdex/redux/store'; +import type { CalcdexBattleField, CalcdexPlayerKey } from '@showdex/redux/store'; import styles from './FieldCalc.module.scss'; interface FieldCalcProps { className?: string; style?: React.CSSProperties; battleId?: string; + playerKey?: CalcdexPlayerKey; field?: CalcdexBattleField; onFieldChange?: (field: DeepPartial) => void; } @@ -20,6 +21,7 @@ export const FieldCalc = ({ className, style, battleId, + playerKey = 'p1', field, onFieldChange, }: FieldCalcProps): JSX.Element => { @@ -28,10 +30,13 @@ export const FieldCalc = ({ const { weather, terrain, - attackerSide, - defenderSide, + attackerSide: p1Side, + defenderSide: p2Side, } = field || {}; + const attackerSide = playerKey === 'p1' ? p1Side : p2Side; + const defenderSide = playerKey === 'p1' ? p2Side : p1Side; + return ( Date: Mon, 15 Aug 2022 04:32:46 -0700 Subject: [PATCH 040/143] feat(calcdex): passed playerKey to FieldCalc as prop --- src/pages/Calcdex/Calcdex.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/pages/Calcdex/Calcdex.tsx b/src/pages/Calcdex/Calcdex.tsx index c7880a00..4189b170 100644 --- a/src/pages/Calcdex/Calcdex.tsx +++ b/src/pages/Calcdex/Calcdex.tsx @@ -36,10 +36,10 @@ export const Calcdex = ({ l.debug( 'rendering...', + '\n', 'colorScheme', colorScheme, '\n', 'p1.pokemon', battle?.p1?.pokemon, '\n', 'p2.pokemon', battle?.p2?.pokemon, '\n', 'state', state, - '\n', 'colorScheme', colorScheme, ); const { @@ -56,9 +56,13 @@ export const Calcdex = ({ const opponentKey = playerKey.current === 'p1' ? 'p2' : 'p1'; React.useEffect(() => { + if (playerKey.current) { + return; + } + const detectedKey = detectPlayerKeyFromBattle(battle); - if (!playerKey.current && detectedKey) { + if (detectedKey) { playerKey.current = detectedKey; } }, [ @@ -110,6 +114,7 @@ export const Calcdex = ({ From 993c537b5b350dfe5aede6fd4b848133518c520c Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Mon, 15 Aug 2022 04:33:43 -0700 Subject: [PATCH 041/143] fix(calcdex): fixed crashing on gen 7 item change in PokeInfo --- src/pages/Calcdex/PokeInfo.tsx | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/pages/Calcdex/PokeInfo.tsx b/src/pages/Calcdex/PokeInfo.tsx index dfa2e555..3730c1ef 100644 --- a/src/pages/Calcdex/PokeInfo.tsx +++ b/src/pages/Calcdex/PokeInfo.tsx @@ -17,9 +17,13 @@ import { } from '@showdex/consts'; import { openSmogonUniversity } from '@showdex/utils/app'; import { detectToggledAbility } from '@showdex/utils/battle'; -import { calcPokemonHp } from '@showdex/utils/calc'; -import type { AbilityName, ItemName } from '@pkmn/data'; -import type { GenerationNum } from '@pkmn/types'; +import { calcPokemonHp, calcPokemonStats } from '@showdex/utils/calc'; +import type { + AbilityName, + Generation, + GenerationNum, + ItemName, +} from '@pkmn/data'; import type { CalcdexPokemon, CalcdexPokemonPreset } from '@showdex/redux/store'; import { usePresets } from './usePresets'; import styles from './PokeInfo.module.scss'; @@ -27,6 +31,7 @@ import styles from './PokeInfo.module.scss'; export interface PokeInfoProps { className?: string; style?: React.CSSProperties; + dex?: Generation; gen?: GenerationNum; format?: string; pokemon: CalcdexPokemon; @@ -36,6 +41,7 @@ export interface PokeInfoProps { export const PokeInfo = ({ className, style, + dex, gen, format, pokemon, @@ -118,8 +124,17 @@ export const PokeInfo = ({ mutation.dirtyItem = null; } + // calculate the stats with the EVs/IVs + if (typeof dex?.stats?.calc === 'function') { + mutation.calculatedStats = calcPokemonStats(dex, { + ...pokemon, + ...mutation, + }); + } + onPokemonChange?.(mutation); }, [ + dex, onPokemonChange, pokemon, ]); @@ -156,7 +171,7 @@ export const PokeInfo = ({ piconStyle={pokemon?.name ? { transform: 'scaleX(-1)' } : undefined} pokemon={{ ...pokemon, - speciesForme: pokemon?.rawSpeciesForme ?? pokemon?.speciesForme, + speciesForme: pokemon?.speciesForme || pokemon?.rawSpeciesForme, item: pokemon?.dirtyItem ?? pokemon?.item, }} tooltip="Open Smogon Page" @@ -465,7 +480,7 @@ export const PokeInfo = ({ }, !!BattleItems && { label: 'All', options: Object.values(BattleItems) - .filter((i) => i?.name && (!pokemon?.altItems?.length || !pokemon.altItems.includes(i.name as ItemName))) + .filter((i) => i?.name && (gen === 7 || (!i.megaStone && !i.zMove)) && (!pokemon?.altItems?.length || !pokemon.altItems.includes(i.name as ItemName))) .map((item) => ({ label: item.name, value: item.name })), }].filter(Boolean)} noOptionsMessage="No Items" From 287274eb240ed67e10d11ff30315a907f11d0c9f Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Mon, 15 Aug 2022 04:34:58 -0700 Subject: [PATCH 042/143] feat(utils): simplified stat boost delta detection --- src/utils/battle/detectStatBoostDelta.ts | 68 +++++++++++++----------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/src/utils/battle/detectStatBoostDelta.ts b/src/utils/battle/detectStatBoostDelta.ts index 075ed880..7592259f 100644 --- a/src/utils/battle/detectStatBoostDelta.ts +++ b/src/utils/battle/detectStatBoostDelta.ts @@ -14,51 +14,55 @@ export type PokemonStatBoostDelta = */ export const detectStatBoostDelta = ( pokemon: CalcdexPokemon, + finalStats: Showdown.StatsTable, stat: Showdown.StatName, ): PokemonStatBoostDelta => { - if (stat === 'hp') { - return null; - } + // if (stat === 'hp') { + // return null; + // } // check for boosts from abilities - if ('slowstart' in (pokemon?.volatiles || {}) && pokemon?.abilityToggled) { - if (['atk', 'spe'].includes(stat)) { - return 'negative'; - } - } + // if ('slowstart' in (pokemon?.volatiles || {}) && pokemon?.abilityToggled) { + // if (['atk', 'spe'].includes(stat)) { + // return 'negative'; + // } + // } // check for status-dependent boosts from abilities - const abilitySearchString = pokemon?.ability?.toLowerCase?.(); - const hasGuts = abilitySearchString === 'guts'; - const hasQuickFeet = abilitySearchString === 'quick feet'; - - if (pokemon?.status && pokemon.status !== '???') { - if (hasGuts && stat === 'atk') { - return 'positive'; - } + // const abilitySearchString = pokemon?.ability?.toLowerCase?.(); + // const hasGuts = abilitySearchString === 'guts'; + // const hasQuickFeet = abilitySearchString === 'quick feet'; - if (hasQuickFeet && stat === 'spe') { - return 'positive'; - } + // if (pokemon?.status && pokemon.status !== '???') { + // if (hasGuts && stat === 'atk') { + // return 'positive'; + // } + // + // if (hasQuickFeet && stat === 'spe') { + // return 'positive'; + // } + // + // // may be problematic since we're not using the Pokemon's base stats, + // // but oh well, this ok for now lmaoo + // if (pokemon.status === 'brn' && stat === 'atk') { + // return 'negative'; + // } + // + // if (pokemon.status === 'par' && stat === 'spe') { + // return 'negative'; + // } + // } - // may be problematic since we're not using the Pokemon's base stats, - // but oh well, this ok for now lmaoo - if (pokemon.status === 'brn' && stat === 'atk') { - return 'negative'; - } - - if (pokemon.status === 'par' && stat === 'spe') { - return 'negative'; - } - } + // const boost = pokemon?.dirtyBoosts?.[stat] ?? pokemon?.boosts?.[stat] ?? 0; - const boost = pokemon?.dirtyBoosts?.[stat] ?? pokemon?.boosts?.[stat] ?? 0; + const calculatedStat = pokemon?.calculatedStats?.[stat] ?? 0; + const finalStat = finalStats?.[stat] ?? 0; - if (boost > 0) { + if (finalStat > calculatedStat) { return 'positive'; } - if (boost < 0) { + if (finalStat < calculatedStat) { return 'negative'; } From 193d224f3d16824bf227116f8bf9f74e67d82936 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Mon, 15 Aug 2022 04:36:11 -0700 Subject: [PATCH 043/143] feat(calcdex): bolded some headers and labels --- .../layout/TableGrid/TableGrid.module.scss | 1 + src/pages/Calcdex/FieldCalc.module.scss | 1 + src/pages/Calcdex/PlayerCalc.module.scss | 1 + src/pages/Calcdex/PokeInfo.module.scss | 22 ++++++++++--------- src/pages/Calcdex/PokeMoves.module.scss | 1 + src/pages/Calcdex/PokeStats.module.scss | 1 + 6 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/components/layout/TableGrid/TableGrid.module.scss b/src/components/layout/TableGrid/TableGrid.module.scss index ae8d005a..c1e117d9 100644 --- a/src/components/layout/TableGrid/TableGrid.module.scss +++ b/src/components/layout/TableGrid/TableGrid.module.scss @@ -24,6 +24,7 @@ } .header { + font-weight: 500; text-transform: uppercase; user-select: none; diff --git a/src/pages/Calcdex/FieldCalc.module.scss b/src/pages/Calcdex/FieldCalc.module.scss index ecf32404..dd823298 100644 --- a/src/pages/Calcdex/FieldCalc.module.scss +++ b/src/pages/Calcdex/FieldCalc.module.scss @@ -9,6 +9,7 @@ .label { @include spacing.margin($bottom: 1px); + font-weight: 500; } button.toggleButton { diff --git a/src/pages/Calcdex/PlayerCalc.module.scss b/src/pages/Calcdex/PlayerCalc.module.scss index 0e7d88b9..29ba7820 100644 --- a/src/pages/Calcdex/PlayerCalc.module.scss +++ b/src/pages/Calcdex/PlayerCalc.module.scss @@ -26,6 +26,7 @@ button.usernameButton { } .usernameButtonLabel { + font-weight: 500; text-align: left; text-overflow: ellipsis; white-space: nowrap; diff --git a/src/pages/Calcdex/PokeInfo.module.scss b/src/pages/Calcdex/PokeInfo.module.scss index 678b86f5..3b6f7d2d 100644 --- a/src/pages/Calcdex/PokeInfo.module.scss +++ b/src/pages/Calcdex/PokeInfo.module.scss @@ -31,6 +31,7 @@ } .label { + font-weight: 500; text-transform: uppercase; user-select: none; @@ -65,21 +66,12 @@ display: inline-block; vertical-align: top; max-width: 45%; + font-weight: 500; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; } -.currentHp { - .light & { - color: color.alpha(colors.$black, 0.75); - } - - .dark & { - color: color.alpha(colors.$white, 0.75); - } -} - .toggleButtonLabel { .light &.inactive { color: colors.$black; @@ -108,6 +100,16 @@ @include spacing.margin($bottom: 2px); } +.currentHp { + .light & { + color: color.alpha(colors.$black, 0.75); + } + + .dark & { + color: color.alpha(colors.$white, 0.75); + } +} + .presetContainer { flex: 1; } diff --git a/src/pages/Calcdex/PokeMoves.module.scss b/src/pages/Calcdex/PokeMoves.module.scss index cc5fd9c5..d3f93303 100644 --- a/src/pages/Calcdex/PokeMoves.module.scss +++ b/src/pages/Calcdex/PokeMoves.module.scss @@ -39,6 +39,7 @@ } .label { + font-weight: 500; text-transform: uppercase; user-select: none; diff --git a/src/pages/Calcdex/PokeStats.module.scss b/src/pages/Calcdex/PokeStats.module.scss index bdd60fe7..1511c0be 100644 --- a/src/pages/Calcdex/PokeStats.module.scss +++ b/src/pages/Calcdex/PokeStats.module.scss @@ -15,6 +15,7 @@ } .statHeader { + font-weight: 500; @include transition.apply(color); &.up { From 9b75d74d15f929c70ad0a91c3e0bce1c099b3fe5 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Mon, 15 Aug 2022 04:37:00 -0700 Subject: [PATCH 044/143] fix(calcdex): sanitized transformed moves in PokeMoves --- src/pages/Calcdex/PokeMoves.tsx | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/pages/Calcdex/PokeMoves.tsx b/src/pages/Calcdex/PokeMoves.tsx index 588690e8..02fbec62 100644 --- a/src/pages/Calcdex/PokeMoves.tsx +++ b/src/pages/Calcdex/PokeMoves.tsx @@ -108,11 +108,12 @@ export const PokeMoves = ({ {/* (actual) moves */} {Array(movesCount).fill(null).map((_, i) => { const moveid = pokemon?.moves?.[i]; - const move = moveid ? dex?.moves?.get?.(moveid) : null; const transformed = !!moveid && moveid?.charAt(0) === '*'; // moves used by a transformed Ditto const moveName = (transformed ? moveid.substring(1) : moveid) as MoveName; + const move = moveid ? dex?.moves?.get?.(moveName) : null; + // if (pokemon?.useUltimateMoves) { // const ultName = gen === 7 ? // getZMove(dex, moveName, pokemon?.dirtyItem ?? pokemon?.item) : @@ -196,7 +197,7 @@ export const PokeMoves = ({ return; } - moves[i] = newMove; + moves[i] = newMove.replace('*', '') as MoveName; onPokemonChange?.({ moves, @@ -211,25 +212,40 @@ export const PokeMoves = ({ getMaxMove(dex, name, pokemon?.dirtyAbility ?? pokemon?.ability, pokemon?.rawSpeciesForme); if (ultName) { - return { label: ultName, value: name }; + return { + label: ultName, + value: name, + }; } return null; }).filter(Boolean), }, !!pokemon?.moveState?.revealed.length && { label: 'Revealed', - options: pokemon.moveState.revealed.map((name) => ({ label: name, value: name })), + options: pokemon.moveState.revealed.map((name) => ({ + label: name.replace('*', '') as MoveName, + value: name.replace('*', '') as MoveName, + })), }, !!pokemon?.altMoves?.length && { label: 'Pool', options: pokemon.altMoves .filter((n) => !!n && (!pokemon.moveState?.revealed?.length || !pokemon.moveState.revealed.includes(n))) - .map((name) => ({ label: name, value: name })), + .map((name) => ({ + label: name, + value: name, + })), }, !!pokemon?.moveState?.learnset.length && { label: 'Learnset', - options: pokemon.moveState.learnset.map((name) => ({ label: name, value: name })), + options: pokemon.moveState.learnset.map((name) => ({ + label: name as MoveName, + value: name as MoveName, + })), }, !!pokemon?.moveState?.other.length && { label: 'All', - options: pokemon.moveState.other.map((name) => ({ label: name, value: name })), + options: pokemon.moveState.other.map((name) => ({ + label: name as MoveName, + value: name as MoveName, + })), }].filter(Boolean)} noOptionsMessage="No Moves Found" disabled={!pokemon?.speciesForme} From 7d2ab8628495c7cdea5d734e8ab7b7853ff3decb Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Mon, 15 Aug 2022 04:37:53 -0700 Subject: [PATCH 045/143] feat(calcdex): added props for calcPokemonFinalStats --- src/pages/Calcdex/PlayerCalc.tsx | 8 ++++++-- src/pages/Calcdex/PokeCalc.tsx | 10 +++++++++- src/pages/Calcdex/PokeStats.tsx | 18 +++++++++++------- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/pages/Calcdex/PlayerCalc.tsx b/src/pages/Calcdex/PlayerCalc.tsx index b8b3cce3..f69a5bc4 100644 --- a/src/pages/Calcdex/PlayerCalc.tsx +++ b/src/pages/Calcdex/PlayerCalc.tsx @@ -3,6 +3,7 @@ import cx from 'classnames'; import { PiconButton, useColorScheme } from '@showdex/components/app'; import { Button } from '@showdex/components/ui'; import { openShowdownUser } from '@showdex/utils/app'; +import { env } from '@showdex/utils/core'; import type { Generation } from '@pkmn/data'; import type { GenerationNum } from '@pkmn/types'; import type { @@ -120,8 +121,9 @@ export const PlayerCalc = ({
- {Array(6).fill(null).map((_, i) => { + {Array(env.int('calcdex-player-max-pokemon', 6)).fill(null).map((_, i) => { const mon = pokemon?.[i]; + const pokemonKey = mon?.calcdexId || mon?.ident || defaultName || '???'; const friendlyPokemonName = mon?.rawSpeciesForme || mon?.speciesForme || mon?.name || pokemonKey; @@ -139,7 +141,7 @@ export const PlayerCalc = ({ aria-label={`Select ${friendlyPokemonName}`} pokemon={mon ? { ...mon, - speciesForme: mon?.rawSpeciesForme ?? mon?.speciesForme, + speciesForme: mon?.speciesForme || mon?.rawSpeciesForme, item: mon?.dirtyItem ?? mon?.item, } : 'pokeball-none'} tooltip={friendlyPokemonName} /** @todo make this more descriptive, like the left-half of PokeInfo */ @@ -158,6 +160,7 @@ export const PlayerCalc = ({ dex={dex} gen={gen} format={format} + playerKey={playerKey} playerPokemon={playerPokemon} opponentPokemon={opponentPokemon} field={{ @@ -165,6 +168,7 @@ export const PlayerCalc = ({ attackerSide: playerSideId === playerKey ? field?.attackerSide : field?.defenderSide, defenderSide: playerSideId === playerKey ? field?.defenderSide : field?.attackerSide, }} + side={playerSideId === playerKey ? 'attacker' : 'defender'} onPokemonChange={(p) => onPokemonChange?.(playerKey, p)} />
diff --git a/src/pages/Calcdex/PokeCalc.tsx b/src/pages/Calcdex/PokeCalc.tsx index cb023b85..de930197 100644 --- a/src/pages/Calcdex/PokeCalc.tsx +++ b/src/pages/Calcdex/PokeCalc.tsx @@ -3,7 +3,7 @@ import cx from 'classnames'; // import { logger } from '@showdex/utils/debug'; import type { Generation } from '@pkmn/data'; import type { GenerationNum } from '@pkmn/types'; -import type { CalcdexBattleField, CalcdexPokemon } from '@showdex/redux/store'; +import type { CalcdexBattleField, CalcdexPlayerKey, CalcdexPokemon } from '@showdex/redux/store'; import { PokeInfo } from './PokeInfo'; import { PokeMoves } from './PokeMoves'; import { PokeStats } from './PokeStats'; @@ -16,9 +16,11 @@ interface PokeCalcProps { dex?: Generation; gen?: GenerationNum; format?: string; + playerKey?: CalcdexPlayerKey; playerPokemon: CalcdexPokemon; opponentPokemon: CalcdexPokemon; field?: CalcdexBattleField; + side?: 'attacker' | 'defender'; onPokemonChange?: (pokemon: DeepPartial) => void; } @@ -30,15 +32,18 @@ export const PokeCalc = ({ dex, gen, format, + playerKey, playerPokemon, opponentPokemon, field, + side, onPokemonChange, }: PokeCalcProps): JSX.Element => { const calculateMatchup = useSmogonMatchup( dex, playerPokemon, opponentPokemon, + playerKey, field, ); @@ -98,6 +103,7 @@ export const PokeCalc = ({ > {/* name, types, level, HP, status, set, ability, nature, item */}
diff --git a/src/pages/Calcdex/PokeStats.tsx b/src/pages/Calcdex/PokeStats.tsx index b2bacc52..f0bb9a60 100644 --- a/src/pages/Calcdex/PokeStats.tsx +++ b/src/pages/Calcdex/PokeStats.tsx @@ -10,9 +10,9 @@ import { PokemonStatNames, } from '@showdex/consts'; import { detectStatBoostDelta, formatStatBoost } from '@showdex/utils/battle'; -import { calcPokemonStats } from '@showdex/utils/calc'; +import { calcPokemonFinalStats } from '@showdex/utils/calc'; import type { Generation } from '@pkmn/data'; -import type { CalcdexPokemon } from '@showdex/redux/store'; +import type { CalcdexBattleField, CalcdexPokemon } from '@showdex/redux/store'; import styles from './PokeStats.module.scss'; export interface PokeStatsProps { @@ -20,6 +20,8 @@ export interface PokeStatsProps { style?: React.CSSProperties; dex: Generation; pokemon: CalcdexPokemon; + field?: CalcdexBattleField; + side?: 'attacker' | 'defender'; onPokemonChange?: (pokemon: DeepPartial) => void; } @@ -28,6 +30,8 @@ export const PokeStats = ({ style, dex, pokemon, + field, + side, onPokemonChange, }: PokeStatsProps): JSX.Element => { const colorScheme = useColorScheme(); @@ -35,9 +39,9 @@ export const PokeStats = ({ const pokemonKey = pokemon?.calcdexId || pokemon?.name || '???'; const friendlyPokemonName = pokemon?.speciesForme || pokemon?.name || pokemonKey; - const calculatedStats = React.useMemo( - () => (pokemon?.calcdexId ? calcPokemonStats(dex, pokemon) : null), - [dex, pokemon], + const finalStats = React.useMemo( + () => (pokemon?.calcdexId ? calcPokemonFinalStats(dex, pokemon, field, side) : null), + [dex, pokemon, field, side], ); return ( @@ -149,9 +153,9 @@ export const PokeStats = ({ {PokemonStatNames.map((stat) => { - const calculatedStat = calculatedStats?.[stat] || 0; + const calculatedStat = finalStats?.[stat] || 0; const formattedStat = formatStatBoost(calculatedStat) || '???'; - const boostDelta = detectStatBoostDelta(pokemon, stat); + const boostDelta = detectStatBoostDelta(pokemon, finalStats, stat); return ( Date: Mon, 15 Aug 2022 04:38:41 -0700 Subject: [PATCH 046/143] fix(calcdex): reverted to using pkmn dex over bastardized dex --- src/pages/Calcdex/useCalcdex.ts | 112 ++++++++++++-------------------- 1 file changed, 41 insertions(+), 71 deletions(-) diff --git a/src/pages/Calcdex/useCalcdex.ts b/src/pages/Calcdex/useCalcdex.ts index 68031ec9..ba3b6b30 100644 --- a/src/pages/Calcdex/useCalcdex.ts +++ b/src/pages/Calcdex/useCalcdex.ts @@ -11,21 +11,15 @@ import type { CalcdexPlayerKey, CalcdexPokemon, } from '@showdex/redux/store'; -// import { usePresetCache } from './usePresetCache'; export interface CalcdexHookProps { battle: Showdown.Battle; - // tooltips: Showdown.BattleTooltips; - // smogon: PkmnSmogon; } export interface CalcdexHookInterface { dex: Generation; state: CalcdexBattleState; - // dispatch: CalcdexReducerDispatch; - // addPokemon: (pokemon: Partial) => Promise; updatePokemon: (playerKey: CalcdexPlayerKey, pokemon: DeepPartial) => void; - // syncPokemon: (pokemon: Partial) => void; updateField: (field: DeepPartial) => void; setActiveIndex: (playerKey: CalcdexPlayerKey, activeIndex: number) => void; setSelectionIndex: (playerKey: CalcdexPlayerKey, selectionIndex: number) => void; @@ -36,67 +30,57 @@ const l = logger('@showdex/pages/Calcdex/useCalcdex'); // we're using the `Dex` from `window.Dex` that the Showdown client uses // const gens = new Generations(($.extend(true, PkmnDex, Dex) as unknown) as ModdedDex); -const gens = new Generations(PkmnDex); +// const gens = new Generations(PkmnDex); export const useCalcdex = ({ battle, - // tooltips, - // smogon, }: CalcdexHookProps): CalcdexHookInterface => { - // const [state, dispatch] = useThunkyReducer( - // CalcdexReducer, - // { ...CalcdexInitialState }, - // ); - const state = useCalcdexState(); + const calcdexState = useCalcdexState(); const dispatch = useDispatch(); - const battleState = state[battle?.id]; + const battleState = calcdexState[battle?.id]; l.debug( - '\n', 'state', state, + '\n', 'calcdexState', calcdexState, '\n', 'battleState', battleState, ); - // const [prevNonce, setPrevNonce] = React.useState(null); - // const presetCache = usePresetCache(); - const gen = battle?.gen as GenerationNum; const format = battle?.id?.split?.('-')?.[1]; - const pkmnDex = gens.get(gen); - // const dex = React.useMemo(() => (typeof gen === 'number' && gen > 0 ? gens.get(gen) : null), [gen]); - const dex = React.useMemo(() => (typeof gen === 'number' && gen > 0 ? { - ...pkmnDex, - species: (Dex ?? pkmnDex).species, - num: gen, - } : null), [ - gen, - pkmnDex, - ]); + const gens = React.useRef(new Generations(PkmnDex)); + const dex = gens.current.get(gen); + + // const dex = React.useMemo(() => (typeof gen === 'number' && gen > 0 ? { + // ...pkmnDex, + // species: (Dex ?? pkmnDex).species, + // num: gen, + // } : null), [ + // gen, + // pkmnDex, + // ]); // handles `battle` changes React.useEffect(() => { l.debug( - 'React.useEffect()', 'received battle update; determining sync changes...', '\n', 'battle.nonce', battle?.nonce, '\n', 'battleState.battleNonce', battleState?.battleNonce, '\n', 'battle.p1.pokemon', battle?.p1?.pokemon, '\n', 'battle.p2.pokemon', battle?.p2?.pokemon, '\n', 'battle', battle, - '\n', 'state', state, + '\n', 'calcdexState', calcdexState, ); if (!battle?.p1 && !battle?.p2 && !battle?.p3 && !battle?.p4) { l.debug( - 'React.useEffect()', 'ignoring battle update due to missing players... w0t', '\n', 'battle.p1.pokemon', battle?.p1?.pokemon, '\n', 'battle.p2.pokemon', battle?.p2?.pokemon, '\n', 'battle.p3.pokemon', battle?.p3?.pokemon, '\n', 'battle.p4.pokemon', battle?.p4?.pokemon, '\n', 'battle', battle, - '\n', 'state', state, + '\n', 'calcdexState', calcdexState, ); return; @@ -105,31 +89,16 @@ export const useCalcdex = ({ if (!battle?.nonce) { // this means the passed-in `battle` object is not from the bootstrapper l.debug( - 'React.useEffect()', 'ignoring battle update due to missing nonce', battle?.nonce, '\n', 'battle', battle, - '\n', 'state', state, + '\n', 'calcdexState', calcdexState, ); return; } - // if (battle.nonce === battleState.battleNonce) { - // l.debug( - // 'React.useEffect()', - // 'ignoring battle update since nonce hasn\'t changed', - // '\n', 'battle.nonce', battle.nonce, - // '\n', 'battleState.battleNonce', battleState.battleNonce, - // '\n', 'battle', battle, - // '\n', 'state', state, - // ); - // - // return; - // } - if (!battleState?.battleId) { l.debug( - 'React.useEffect()', 'initializing empty battleState', '\n', 'with battle.nonce', battle.nonce, '\n', 'battle', battle, @@ -138,7 +107,7 @@ export const useCalcdex = ({ dispatch(calcdexSlice.actions.init({ battleId: battle.id, - gen: battle.gen, + gen, format, battleNonce: battle.nonce, p1: { name: battle.p1?.name, rating: battle.p1?.rating }, @@ -146,11 +115,10 @@ export const useCalcdex = ({ })); } else if (!battleState?.battleNonce || battle.nonce !== battleState.battleNonce) { l.debug( - 'React.useEffect()', 'updating battleState via syncBattle()', '\n', 'battle', battle, '\n', 'battleState', battleState, - '\n', 'state', state, + // '\n', 'state', state, ); void dispatch(syncBattle({ @@ -160,20 +128,20 @@ export const useCalcdex = ({ } l.debug( - 'React.useEffect()', - 'completed battle state sync for nonce', battle.nonce, + 'Completed battleState sync for nonce', battle.nonce, '\n', 'battle', battle, - '\n', 'state', state, + '\n', 'battleState', battleState, + // '\n', 'state', state, ); }, [ battle, battle?.nonce, battleState, + calcdexState, dex, dispatch, format, - // presetCache, - state, + gen, ]); return { @@ -184,20 +152,22 @@ export const useCalcdex = ({ gen: null, format: null, field: null, - p1: { - name: null, - rating: null, - activeIndex: -1, - selectionIndex: 0, - pokemon: [], - }, - p2: { - name: null, - rating: null, - activeIndex: -1, - selectionIndex: 0, - pokemon: [], - }, + p1: null, + p2: null, + // p1: { + // name: null, + // rating: null, + // activeIndex: -1, + // selectionIndex: 0, + // pokemon: [], + // }, + // p2: { + // name: null, + // rating: null, + // activeIndex: -1, + // selectionIndex: 0, + // pokemon: [], + // }, }, updatePokemon: (playerKey, pokemon) => dispatch(calcdexSlice.actions.updatePokemon({ From 8713104a93795b69374cee05e75a70a1b11f85e6 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Mon, 15 Aug 2022 04:41:14 -0700 Subject: [PATCH 047/143] fix(utils): tried to fix failed server spread guesses sometimes doesn't guess it tho, resulting in all 0's for EVs/IVs sadge --- src/utils/calc/guessServerSpread.ts | 57 ++++++++++++++++------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/src/utils/calc/guessServerSpread.ts b/src/utils/calc/guessServerSpread.ts index 178d57b8..c3e97f1a 100644 --- a/src/utils/calc/guessServerSpread.ts +++ b/src/utils/calc/guessServerSpread.ts @@ -30,11 +30,14 @@ export const guessServerSpread = ( knownNature?: NatureName, ): Partial => { if (!pokemon?.speciesForme) { - l.warn( - 'received an invalid Pokemon without a speciesForme', - '\n', 'pokemon', pokemon, - '\n', 'serverPokemon', serverPokemon, - ); + if (__DEV__) { + l.warn( + 'Received an invalid Pokemon without a speciesForme', + '\n', 'pokemon', pokemon, + '\n', 'serverPokemon', serverPokemon, + '\n', '(You will only see this warning on development.)', + ); + } return null; } @@ -42,15 +45,16 @@ export const guessServerSpread = ( const species = dex.species.get(pokemon.speciesForme); if (typeof species?.baseStats?.hp !== 'number') { - l.warn( - 'guessServerSpread() <- dex.species.get()', - '\n', 'received no baseStats for the given speciesForme', - '\n', 'speciesForme', pokemon.speciesForme, - '\n', 'species', species, - '\n', 'dex', dex, - '\n', 'pokemon', pokemon, - '\n', 'serverPokemon', serverPokemon, - ); + if (__DEV__) { + l.warn( + '\n', 'Received no baseStats for the given speciesForme', pokemon.speciesForme, + '\n', 'species', species, + '\n', 'dex', dex, + '\n', 'pokemon', pokemon, + '\n', 'serverPokemon', serverPokemon, + '\n', '(You will only see this warning on development.)', + ); + } return null; } @@ -111,7 +115,7 @@ export const guessServerSpread = ( // don't say I didn't warn ya! for (let iv = 31; iv >= 0; iv -= 31) { // try only 31 and 0 for IVs (who assigns any other IVs?) - for (let ev = 252; ev >= 0; ev -= 4) { // try 252 to 0 in multiples of 4 + for (let ev = 0; ev <= 252; ev += 4) { // try 252 to 0 in multiples of 4 calculatedStats[stat] = dex.stats.calc( stat, baseStats[stat], @@ -123,9 +127,9 @@ export const guessServerSpread = ( // warning: if you don't filter this log, there will be lots of logs (and I mean A LOT) // may crash your browser depending on your computer's specs. debug at your own risk! - // if (pokemon.ident.endsWith('Clefable')) { + // if (pokemon.ident.includes('Kyurem')) { // l.debug( - // 'trying to find the spread for', pokemon.ident, 'stat', stat, + // 'Trying to find the spread for', pokemon.ident, 'stat', stat, // '\n', 'calculatedStat', calculatedStats[stat], 'knownStat', knownStats[stat], // '\n', 'iv', iv, 'ev', ev, // '\n', 'nature', nature.name, '+', nature.plus, '-', nature.minus, @@ -135,12 +139,14 @@ export const guessServerSpread = ( if (calculatedStats[stat] === knownStats[stat]) { // this one isn't too bad to print, but will still cause a considerable slowdown // (you should only uncomment the debug log if shit is really hitting the fan) - // l.debug( - // 'found matching combination for', pokemon.ident, 'stat', stat, - // '\n', 'calculatedStat', calculatedStats[stat], 'knownStat', knownStats[stat], - // '\n', 'iv', iv, 'ev', ev, - // '\n', 'nature', nature.name, '+', nature.plus, '-', nature.minus, - // ); + // if (pokemon.ident.includes('Lilligant')) { + // l.debug( + // 'Found matching combination for', pokemon.ident, 'stat', stat, + // '\n', 'calculatedStat', calculatedStats[stat], 'knownStat', knownStats[stat], + // '\n', 'iv', iv, 'ev', ev, + // '\n', 'nature', nature.name, '+', nature.plus, '-', nature.minus, + // ); + // } guessedSpread.ivs[stat] = iv; guessedSpread.evs[stat] = ev; @@ -170,7 +176,7 @@ export const guessServerSpread = ( if (sameStats && evsLegal) { // l.debug( - // 'found nature that matches all of the stats for Pokemon', pokemon.ident, + // 'Found nature that matches all of the stats for Pokemon', pokemon.ident, // '\n', 'nature', nature.name, // '\n', 'calculatedStats', calculatedStats, // '\n', 'knownStats', knownStats, @@ -190,8 +196,7 @@ export const guessServerSpread = ( } l.debug( - 'guessServerSpread() -> return guessedSpread', - '\n', 'returning the best guess of the spread for Pokemon', pokemon.ident, + '\n', 'Returning the best guess of the spread for Pokemon', pokemon.ident, '\n', 'guessedSpread', guessedSpread, '\n', 'dex', dex, '\n', 'pokemon', pokemon, From 218f4b2f4f806a55f354a3ba18c6a37726b137b4 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Wed, 17 Aug 2022 07:17:38 -0700 Subject: [PATCH 048/143] chore: installed react-hotkeys-hook --- package.json | 1 + yarn.lock | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/package.json b/package.json index f8c6e3d0..05c53f71 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "react-dom": "^18.2.0", "react-final-form": "^6.5.9", "react-hot-loader": "^4.13.0", + "react-hotkeys-hook": "^3.4.7", "react-redux": "^8.0.2", "react-select": "^5.4.0", "remove": "^0.1.5", diff --git a/yarn.lock b/yarn.lock index 15df13e4..85051d56 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4208,6 +4208,11 @@ hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react- dependencies: react-is "^16.7.0" +hotkeys-js@3.9.4: + version "3.9.4" + resolved "https://registry.yarnpkg.com/hotkeys-js/-/hotkeys-js-3.9.4.tgz#ce1aa4c3a132b6a63a9dd5644fc92b8a9b9cbfb9" + integrity sha512-2zuLt85Ta+gIyvs4N88pCYskNrxf1TFv3LR9t5mdAZIX8BcgQQ48F2opUptvHa6m8zsy5v/a0i9mWzTrlNWU0Q== + hpack.js@^2.1.6: version "2.1.6" resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" @@ -5789,6 +5794,13 @@ react-hot-loader@^4.13.0: shallowequal "^1.1.0" source-map "^0.7.3" +react-hotkeys-hook@^3.4.7: + version "3.4.7" + resolved "https://registry.yarnpkg.com/react-hotkeys-hook/-/react-hotkeys-hook-3.4.7.tgz#e16a0a85f59feed9f48d12cfaf166d7df4c96b7a" + integrity sha512-+bbPmhPAl6ns9VkXkNNyxlmCAIyDAcWbB76O4I0ntr3uWCRuIQf/aRLartUahe9chVMPj+OEzzfk3CQSjclUEQ== + dependencies: + hotkeys-js "3.9.4" + react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" From 3c71eb00d4cc3e9316a450c89fa149d81caa7a6b Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Wed, 17 Aug 2022 07:18:08 -0700 Subject: [PATCH 049/143] refac(comps): cleaned up useTextFieldHandle a bit --- src/components/form/TextField/useTextFieldHandle.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/form/TextField/useTextFieldHandle.ts b/src/components/form/TextField/useTextFieldHandle.ts index 0b83aff1..ce26e258 100644 --- a/src/components/form/TextField/useTextFieldHandle.ts +++ b/src/components/form/TextField/useTextFieldHandle.ts @@ -13,9 +13,10 @@ export const useTextFieldHandle = < input: FieldInputProps, ): void => React.useImperativeHandle(forwardedRef, () => ({ ...ref?.current, - focus: () => { + + focus: (options?: FocusOptions) => { if (typeof ref?.current?.focus === 'function') { - ref.current.focus?.(); + ref.current.focus(options); // place the cursor at the end of the value // (logic will be placed at end of stack by setTimeout, otherwise, cursor may not be moved at all!) From 6c764eacdf682a3ca819c05ea27288af6145894f Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Wed, 17 Aug 2022 07:18:32 -0700 Subject: [PATCH 050/143] fix(comps): removed useTextFieldHandle from BaseTextField --- src/components/form/TextField/BaseTextField.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/form/TextField/BaseTextField.tsx b/src/components/form/TextField/BaseTextField.tsx index 0816b234..01bd25f8 100644 --- a/src/components/form/TextField/BaseTextField.tsx +++ b/src/components/form/TextField/BaseTextField.tsx @@ -4,7 +4,7 @@ import cx from 'classnames'; import type { AriaTextFieldProps } from '@react-types/textfield'; import type { TextFieldAria as ITextFieldAria } from '@react-aria/textfield'; import type { FieldRenderProps } from 'react-final-form'; -import { useTextFieldHandle } from './useTextFieldHandle'; +// import { useTextFieldHandle } from './useTextFieldHandle'; import styles from './BaseTextField.module.scss'; export type TextFieldElement = HTMLInputElement | HTMLTextAreaElement; @@ -83,7 +83,12 @@ export const BaseTextField = React.forwardRef inputRef.current, + ); return ( <> From 73e5f07745ff217e5cbfa23e998183383158d18e Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Wed, 17 Aug 2022 07:19:41 -0700 Subject: [PATCH 051/143] feat(comps): manually handled ValueField input value changes --- src/components/form/ValueField/ValueField.tsx | 220 +++++++++++++++--- 1 file changed, 187 insertions(+), 33 deletions(-) diff --git a/src/components/form/ValueField/ValueField.tsx b/src/components/form/ValueField/ValueField.tsx index e4cc066c..fe4ac841 100644 --- a/src/components/form/ValueField/ValueField.tsx +++ b/src/components/form/ValueField/ValueField.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; import cx from 'classnames'; import { useColorScheme } from '@showdex/components/app'; import { BaseTextField } from '@showdex/components/form'; @@ -8,6 +9,36 @@ import styles from './ValueField.module.scss'; export interface ValueFieldProps extends BaseTextFieldProps { className?: string; style?: React.CSSProperties; + + /** + * Kinda like the native `step` prop, but for when the user is holding down the `SHIFT` key. + * + * * If unspecified (default), this behavior will be disabled. + * + * @since 0.1.3 + */ + shiftStep?: number; + + /** + * Whether to loop to the `max` value when `min` is reached. + * + * * As implied, `min` and `max` props are required for this to work. + * + * @default false + * @since 0.1.3 + */ + loop?: boolean; + + /** + * Whether to use an absolutely-positioned pseudo-element + * for indicating the input's hover/active state. + * + * * If `true`, padding and hover/active states are applied to the pseudo-element. + * * If `false` (default), padding and hover/active states are applied to the container. + * + * @default false + * @since 0.1.0 + */ absoluteHover?: boolean; } @@ -18,11 +49,24 @@ export const ValueField = React.forwardRef(({ className, style, inputClassName, + min, + max, + step = 1, + shiftStep, + loop, absoluteHover, input, disabled, ...props }: ValueFieldProps, forwardedRef): JSX.Element => { + const inputRef = React.useRef(null); + + React.useImperativeHandle( + forwardedRef, + () => inputRef.current, + ); + + // grab the color scheme for applying the theme const colorScheme = useColorScheme(); // although react-final-form has meta.active, @@ -33,6 +77,143 @@ export const ValueField = React.forwardRef(({ // this is only a visual value, so that we don't forcibly change the user's value as they're typing it const [inputValue, setInputValue] = React.useState(input?.value?.toString()); + // type number fields don't do a good job preventing users from typing in non-numeric characters + // (like '.' and 'e') nor does it enforce the `min` and `max` values if typed in manually. + // hence, we use a regular 'ol type text field and control the value ourselves. yay! + const handleChange = React.useCallback((value: string | number) => { + let strValue = String(value); + + // show empty strings in the input field, but don't update final-form's value + if (!strValue) { + setInputValue(''); + + return; + } + + // remove a manually entered-in minus if min is specified and non-negative + if (strValue === '-' && typeof min === 'number' && min >= 0) { + strValue = ''; + } + + // remove any non-numeric characters + // (except for the leading negative, if present at this point) + strValue = strValue.replace(/(?!^-)[^\d]/g, ''); + + // again, at this point, if we have an empty string, show it, but don't let final-form know + if (!strValue) { + setInputValue(''); + + return; + } + + // convert the strValue to a number and clamp it if min/max props are specified + // (and if all hell breaks loose during this conversion [i.e., NaN], default to 0) + let numValue = Number(strValue) || 0; + + if (typeof min === 'number') { + numValue = loop && typeof max === 'number' && max > min && numValue < min ? + max : + Math.max(min, numValue); + } + + if (typeof max === 'number') { + numValue = loop && typeof min === 'number' && min < max && numValue > max ? + min : + Math.min(numValue, max); + } + + // finally, update the visual value and let final-form know + setInputValue(numValue.toString()); + input?.onChange?.(numValue); + }, [ + input, + loop, + max, + min, + ]); + + const handleBlur = React.useCallback((e?: React.FocusEvent) => { + const strValue = e?.target?.value?.toString(); + + if (typeof strValue === 'string' && strValue !== inputValue) { + handleChange(strValue); + } + + setActive(false); + input?.onBlur?.(e); + }, [ + handleChange, + input, + inputValue, + ]); + + // since we're not using a type number input cause it sucks ass, + // emulate the keyboard controls that it natively provides + const hotkeysRef = useHotkeys([ + typeof step === 'number' && 'up', + typeof shiftStep === 'number' && 'shift+up', + typeof step === 'number' && 'down', + typeof shiftStep === 'number' && 'shift+down', + // 'esc', // doesn't work for some reason sadge :c + 'enter', + ].filter(Boolean).join(', '), (_, handler) => { + const currentValue = Number(input?.value ?? inputValue) || 0; + + console.log('useHotkeys handler.key', handler.key); + + switch (handler.key) { + case 'up': { + handleChange(currentValue + Math.abs(step)); + + break; + } + + case 'shift+up': { + handleChange(currentValue + Math.abs(shiftStep)); + + break; + } + + case 'down': { + handleChange(currentValue - Math.abs(step)); + + break; + } + + case 'shift+down': { + handleChange(currentValue - Math.abs(shiftStep)); + + break; + } + + // case 'esc': + case 'enter': { + // this will also invoke handleBlur() since the input's onBlur() will fire + inputRef.current?.blur?.(); + + break; + } + + default: { + break; + } + } + }, { + enabled: !disabled, + enableOnTags: ['INPUT'], + }, [ + input?.value, + inputValue, + step, + shiftStep, + ]); + + React.useImperativeHandle( + hotkeysRef, + () => inputRef.current, + ); + + // handle updates in final-form's input.value React.useEffect(() => { const value = input?.value?.toString(); @@ -60,48 +241,21 @@ export const ValueField = React.forwardRef(({ style={style} > { - // always accept the user's input to show immediate UI feedback - // (will be reverted to the form's value in onBlur() should this value be invalid) - setInputValue(value); - - const parsedValue = value?.replace?.(/[^\d\.-]/g, ''); - const num = Number(parsedValue); - - if (Number.isNaN(num)) { - return; - } - - input?.onChange?.(num); - }, - - onFocus: () => { + onChange: handleChange, + onFocus: (e?: React.FocusEvent) => { setActive(true); - input?.onFocus?.(); - }, - - onBlur: () => { - setActive(false); - input?.onBlur?.(); - - // if the inputValue doesn't match input.value, revert it back to the form's value - // (should they match, the user has entered in a valid value) - const strValue = input?.value?.toString(); - - if (inputValue !== strValue) { - setInputValue(strValue); - } + input?.onFocus?.(e); }, + onBlur: handleBlur, }} disabled={disabled} /> From 5fcb9f2f14fd68f3550939512a69a48bfd6d8cb7 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Wed, 17 Aug 2022 07:21:38 -0700 Subject: [PATCH 052/143] fix(utils): fixed deleted volatiles in sanitizePokemon idk what I was thinking, this straight up removes ALL volatiles because I forgot to find the typeof the second element of the array value of each volatile object entry, duh --- src/utils/battle/sanitizePokemon.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/utils/battle/sanitizePokemon.ts b/src/utils/battle/sanitizePokemon.ts index 2acaf9b0..d1bd3458 100644 --- a/src/utils/battle/sanitizePokemon.ts +++ b/src/utils/battle/sanitizePokemon.ts @@ -98,7 +98,18 @@ export const sanitizePokemon = ( toxicTurns: pokemon?.statusData?.toxicTurns || 0, }, - volatiles: pokemon?.volatiles, + // only deep-copy non-object volatiles + // (particularly Ditto's `transformed` volatile, which references an existing Pokemon object as its value) + volatiles: Object.entries(pokemon?.volatiles || {}).reduce((volatiles, [id, volatile]) => { + const [, value] = volatile || []; + + if (!value?.[1] || ['string', 'number'].includes(typeof value[1])) { + volatiles[id] = JSON.parse(JSON.stringify(volatile)); + } + + return volatiles; + }, {}), + turnstatuses: pokemon?.turnstatuses, toxicCounter: pokemon?.statusData?.toxicTurns, @@ -156,14 +167,6 @@ export const sanitizePokemon = ( } } - // remove any non-string volatiles - // (particularly Ditto's `transformed` volatile, which references an existing Pokemon object as its value) - Object.entries(sanitizedPokemon.volatiles || {}).forEach(([key, value]) => { - if (!['string', 'number'].includes(typeof value)) { - delete sanitizedPokemon.volatiles[key]; - } - }); - if (!sanitizedPokemon?.calcdexId) { sanitizedPokemon.calcdexId = calcPokemonCalcdexId(sanitizedPokemon); } From fba42e134daa061a7947230c16fed28a0491f85e Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Wed, 17 Aug 2022 07:22:14 -0700 Subject: [PATCH 053/143] feat(calcdex): specified new ValueField props in PokeStats --- src/pages/Calcdex/PokeStats.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pages/Calcdex/PokeStats.tsx b/src/pages/Calcdex/PokeStats.tsx index f0bb9a60..f55dbbce 100644 --- a/src/pages/Calcdex/PokeStats.tsx +++ b/src/pages/Calcdex/PokeStats.tsx @@ -100,6 +100,8 @@ export const PokeStats = ({ min={0} max={31} step={1} + shiftStep={5} + loop input={{ value: iv, onChange: (value: number) => onPokemonChange?.({ @@ -136,6 +138,8 @@ export const PokeStats = ({ min={0} max={252} step={4} + shiftStep={16} + loop input={{ value: ev, onChange: (value: number) => onPokemonChange?.({ From b32b14e67457f74f4b8be4367e7f32e0e35c9a00 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Fri, 19 Aug 2022 21:27:48 -0700 Subject: [PATCH 054/143] chore: disabled preset env debug in babel config --- babel.config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/babel.config.json b/babel.config.json index 1f334962..be4bf18b 100644 --- a/babel.config.json +++ b/babel.config.json @@ -4,7 +4,7 @@ [ "@babel/preset-env", { - "debug": true, + "debug": false, "modules": false, "targets": { "node": "current" From 123420f4bc289b24dc96dd9b8100a220ea9c5e35 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Sat, 20 Aug 2022 01:00:22 -0700 Subject: [PATCH 055/143] feat(comps): added fallbackValue prop to ValueField --- src/components/form/ValueField/ValueField.tsx | 42 +++++++++++++------ 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/src/components/form/ValueField/ValueField.tsx b/src/components/form/ValueField/ValueField.tsx index fe4ac841..41a3619f 100644 --- a/src/components/form/ValueField/ValueField.tsx +++ b/src/components/form/ValueField/ValueField.tsx @@ -10,6 +10,18 @@ export interface ValueFieldProps extends BaseTextFieldProps { className?: string; style?: React.CSSProperties; + /** + * Fallback value for when the input is blurred with an empty string. + * + * * Also used as the fallback value for the internal `inputValue` state, + * should `input.value` be falsy upon initial mount. + * * If not provided (default), the input value will be set to the last valid value, + * which is stored in the `input.value` prop. + * + * @since 0.1.3 + */ + fallbackValue?: number; + /** * Kinda like the native `step` prop, but for when the user is holding down the `SHIFT` key. * @@ -49,6 +61,7 @@ export const ValueField = React.forwardRef(({ className, style, inputClassName, + fallbackValue, min, max, step = 1, @@ -75,7 +88,10 @@ export const ValueField = React.forwardRef(({ const [active, setActive] = React.useState(false); // this is only a visual value, so that we don't forcibly change the user's value as they're typing it - const [inputValue, setInputValue] = React.useState(input?.value?.toString()); + const [inputValue, setInputValue] = React.useState( + input?.value?.toString() + || fallbackValue?.toString(), + ); // type number fields don't do a good job preventing users from typing in non-numeric characters // (like '.' and 'e') nor does it enforce the `min` and `max` values if typed in manually. @@ -111,15 +127,15 @@ export const ValueField = React.forwardRef(({ let numValue = Number(strValue) || 0; if (typeof min === 'number') { - numValue = loop && typeof max === 'number' && max > min && numValue < min ? - max : - Math.max(min, numValue); + numValue = loop && typeof max === 'number' && max > min && numValue < min + ? max + : Math.max(min, numValue); } if (typeof max === 'number') { - numValue = loop && typeof min === 'number' && min < max && numValue > max ? - min : - Math.min(numValue, max); + numValue = loop && typeof min === 'number' && min < max && numValue > max + ? min + : Math.min(numValue, max); } // finally, update the visual value and let final-form know @@ -133,7 +149,7 @@ export const ValueField = React.forwardRef(({ ]); const handleBlur = React.useCallback((e?: React.FocusEvent) => { - const strValue = e?.target?.value?.toString(); + const strValue = (e?.target?.value || fallbackValue)?.toString(); if (typeof strValue === 'string' && strValue !== inputValue) { handleChange(strValue); @@ -142,6 +158,7 @@ export const ValueField = React.forwardRef(({ setActive(false); input?.onBlur?.(e); }, [ + fallbackValue, handleChange, input, inputValue, @@ -154,13 +171,11 @@ export const ValueField = React.forwardRef(({ typeof shiftStep === 'number' && 'shift+up', typeof step === 'number' && 'down', typeof shiftStep === 'number' && 'shift+down', - // 'esc', // doesn't work for some reason sadge :c + 'esc', 'enter', ].filter(Boolean).join(', '), (_, handler) => { const currentValue = Number(input?.value ?? inputValue) || 0; - console.log('useHotkeys handler.key', handler.key); - switch (handler.key) { case 'up': { handleChange(currentValue + Math.abs(step)); @@ -186,7 +201,7 @@ export const ValueField = React.forwardRef(({ break; } - // case 'esc': + case 'esc': case 'enter': { // this will also invoke handleBlur() since the input's onBlur() will fire inputRef.current?.blur?.(); @@ -200,8 +215,9 @@ export const ValueField = React.forwardRef(({ } }, { enabled: !disabled, - enableOnTags: ['INPUT'], + enableOnTags: active ? ['INPUT'] : undefined, }, [ + active, input?.value, inputValue, step, From 0c9e1f2ee9bbab257dae4e5fbf10201e2669162b Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Sat, 20 Aug 2022 01:01:08 -0700 Subject: [PATCH 056/143] docs(consts): added note about multiscale toggleable ability --- src/consts/abilities.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/consts/abilities.ts b/src/consts/abilities.ts index abb02a17..07d43800 100644 --- a/src/consts/abilities.ts +++ b/src/consts/abilities.ts @@ -12,7 +12,7 @@ export const PokemonToggleAbilities: AbilityName[] = [ 'Flash Fire', // 'Intimidate', // applies the ATK reduction within `boosts`, so no need to "toggle" this 'Minus', - 'Multiscale', + 'Multiscale', // special case based off the HP, but specified here to allow toggling in the UI 'Plus', 'Slow Start', 'Stakeout', From 7a9b4d6638a27f3142c26c33f355f1617a8ff72a Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Sat, 20 Aug 2022 01:01:42 -0700 Subject: [PATCH 057/143] feat(consts): added objects for initial stats and boosts --- src/consts/stats.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/consts/stats.ts b/src/consts/stats.ts index fea496d8..c4c817b1 100644 --- a/src/consts/stats.ts +++ b/src/consts/stats.ts @@ -7,9 +7,20 @@ export const PokemonStatNames: Showdown.StatName[] = [ 'spe', ]; +export const PokemonInitialStats: Showdown.StatsTable = { + hp: 0, + atk: 0, + def: 0, + spa: 0, + spd: 0, + spe: 0, +}; + /** * Basically `PokemonStatNames` without `'hp'`, * since you can't boost the Pokemon's HP... yet... (right, GameFreak...?) + * + * @since 0.1.0 */ export const PokemonBoostNames: Showdown.StatNameNoHp[] = [ 'atk', @@ -18,3 +29,11 @@ export const PokemonBoostNames: Showdown.StatNameNoHp[] = [ 'spd', 'spe', ]; + +export const PokemonInitialBoosts: Omit = { + atk: 0, + def: 0, + spa: 0, + spd: 0, + spe: 0, +}; From 8fa0949f13f150d802103877a350ef8a67828cbf Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Sat, 20 Aug 2022 01:01:56 -0700 Subject: [PATCH 058/143] feat(consts): added items consts --- src/consts/index.ts | 1 + src/consts/items.ts | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 src/consts/items.ts diff --git a/src/consts/index.ts b/src/consts/index.ts index f4e510c7..1a501358 100644 --- a/src/consts/index.ts +++ b/src/consts/index.ts @@ -2,6 +2,7 @@ export * from './abilities'; export * from './ansiColor'; export * from './formats'; export * from './httpMethod'; +export * from './items'; export * from './natures'; export * from './stats'; export * from './tagTypes'; diff --git a/src/consts/items.ts b/src/consts/items.ts new file mode 100644 index 00000000..7bb5b59b --- /dev/null +++ b/src/consts/items.ts @@ -0,0 +1,11 @@ +import type { ItemName } from '@pkmn/data'; + +export const PokemonSpeedReductionItems: ItemName[] = [ + 'Macho Brace', + 'Power Anklet', + 'Power Band', + 'Power Belt', + 'Power Bracer', + 'Power Lens', + 'Power Weight', +]; From c6ba5acf9d816c11643dc20ef79bad3e8924834b Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Sat, 20 Aug 2022 01:02:16 -0700 Subject: [PATCH 059/143] feat(utils): added formatId app util --- src/utils/app/formatId.ts | 16 ++++++++++++++++ src/utils/app/index.ts | 1 + 2 files changed, 17 insertions(+) create mode 100644 src/utils/app/formatId.ts diff --git a/src/utils/app/formatId.ts b/src/utils/app/formatId.ts new file mode 100644 index 00000000..43515ac2 --- /dev/null +++ b/src/utils/app/formatId.ts @@ -0,0 +1,16 @@ +/** + * Basically the `toID()` global that's already built into the Showdown client, + * but too lazy atm to declare it as a project global. + * + * * Also used as a string sanitizer for string comparisons, + * especially in `calcPokemonFinalStats()` for abilities, items, etc. + * + * @example + * ```ts + * id('Quick Feet'); // 'quickfeet' + * ``` + * @since 0.1.3 + */ +export const formatId = (value: string) => value?.toString?.() + .toLowerCase() + .replace(/[^a-z0-9]/g, ''); diff --git a/src/utils/app/index.ts b/src/utils/app/index.ts index a4f6cb5b..2ab01afa 100644 --- a/src/utils/app/index.ts +++ b/src/utils/app/index.ts @@ -1,4 +1,5 @@ export * from './createSideRoom'; +export * from './formatId'; export * from './getActiveBattle'; export * from './getBattleRoom'; export * from './getColorScheme'; From 68dedcd568d3d25af4f19b8b9c7b2ae1df0b9099 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Sat, 20 Aug 2022 01:02:59 -0700 Subject: [PATCH 060/143] feat: added env for max legal EVs --- .env | 1 + src/utils/core/getEnv.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/.env b/.env index 134944b2..a641e73e 100644 --- a/.env +++ b/.env @@ -18,6 +18,7 @@ UUID_NAMESPACE="b92890ec-ae85-4692-a4b4-e56268f8caa3" CALCDEX_DEFAULT_GEN=8 CALCDEX_PLAYER_MAX_POKEMON=6 +CALCDEX_POKEMON_MAX_LEGAL_EVS=508 ## ## pkmn diff --git a/src/utils/core/getEnv.ts b/src/utils/core/getEnv.ts index 68f9121d..97b0d05f 100644 --- a/src/utils/core/getEnv.ts +++ b/src/utils/core/getEnv.ts @@ -11,6 +11,7 @@ const processEnv: EnvDict = { CALCDEX_DEFAULT_GEN: process.env.CALCDEX_DEFAULT_GEN, CALCDEX_PLAYER_MAX_POKEMON: process.env.CALCDEX_PLAYER_MAX_POKEMON, + CALCDEX_POKEMON_MAX_LEGAL_EVS: process.env.CALCDEX_POKEMON_MAX_LEGAL_EVS, PKMN_PRESETS_BASE_URL: process.env.PKMN_PRESETS_BASE_URL, PKMN_PRESETS_GENS_PATH: process.env.PKMN_PRESETS_GENS_PATH, PKMN_PRESETS_RANDOMS_PATH: process.env.PKMN_PRESETS_RANDOMS_PATH, From 0c2824d36209c59eedc10747f38a17cdd686b979 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Sat, 20 Aug 2022 01:03:43 -0700 Subject: [PATCH 061/143] docs: wrapped examples in utils typedec --- types/utils.d.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/types/utils.d.ts b/types/utils.d.ts index bf8fb6e2..f79d9593 100644 --- a/types/utils.d.ts +++ b/types/utils.d.ts @@ -2,10 +2,12 @@ * Construct a type with the properties of `T` replaced with those of `R`. * * @example + * ```ts * type ItemId = { id: string; getId: () => string; hasId: () => boolean; }; * type ItemNumId = Modify number; }>; * // -> { id: number; getId: () => number; hasId: () => boolean; } * type ItemTId = Modify T; }>; + * ``` * @since 0.1.0 */ declare type Modify = Omit & R; @@ -14,6 +16,7 @@ declare type Modify = Omit & R; * Construct a type with all properties of `T`, including any sub-properties, as partials. * * @example + * ```ts * type Sosig = { * saturation: { * fatness: number; @@ -25,6 +28,8 @@ declare type Modify = Omit & R; * // -> { saturation?: { fatness: number; color: number; }; gain?: number; } * type SosigDeepPartial = DeepPartial; * // -> { saturation?: { fatness?: number; color?: number; }; gain?: number; } + * ``` + * @since 0.1.0 */ declare type DeepPartial = T extends object ? { [P in keyof T]?: T[P] extends string | number | boolean | symbol | Array ? T[P] : DeepPartial; @@ -34,6 +39,7 @@ declare type DeepPartial = T extends object ? { * Construct a literal type with the keys of the indexable type `T` whose types extend the literal type `K`. * * @example + * ```ts * type Sosig = { * fatness: number; * color: number; @@ -42,6 +48,7 @@ declare type DeepPartial = T extends object ? { * }; * type SosigParams = ExtractKeys; * // -> 'fatness' | 'color' | 'gain' + * ``` * @since 0.1.0 */ declare type ExtractKeys = { [I in keyof T]: T[I] extends K ? I : never; }[keyof T]; @@ -52,6 +59,7 @@ declare type ExtractKeys = { [I in keyof T]: T[I] extends K ? I : never; } * * Most useful for declaring the `[key, value]` types in `Object.entries()`. * * @example + * ```ts * const sosig: Sosig = { * fatness: 100, * color: 100, @@ -60,6 +68,7 @@ declare type ExtractKeys = { [I in keyof T]: T[I] extends K ? I : never; } * }; * type SosigEntries = Extract; * // -> (['fatness', number] | ['color', number] | ['gain', number], ['reset', () => void])[] + * ``` * @since 0.1.0 */ declare type Entries = { [K in keyof T]: [K, T[K]] }[keyof T][]; From 777e3b213d10d4ecca903b96bfe0258b38b2b394 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Sat, 20 Aug 2022 23:12:49 -0700 Subject: [PATCH 062/143] fix(redux): properly deep-copied battle state in syncBattle --- src/redux/actions/syncBattle.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/redux/actions/syncBattle.ts b/src/redux/actions/syncBattle.ts index 3fbb36a9..d7820118 100644 --- a/src/redux/actions/syncBattle.ts +++ b/src/redux/actions/syncBattle.ts @@ -60,9 +60,8 @@ export const syncBattle = createAsyncThunk JSON.parse(JSON.stringify(state[battleId])); + // yooo native deep-copying lessgo baby + const battleState = structuredClone(state[battleId]); // l.debug( // '\n', 'pre-copied battleState', state[battleId], From f12cc2efc65d688753fddde8958e0c2c44ac5ba6 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Sat, 20 Aug 2022 23:13:25 -0700 Subject: [PATCH 063/143] feat(utils): updated Pokemon ident detection --- src/utils/battle/detectPokemonIdent.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/utils/battle/detectPokemonIdent.ts b/src/utils/battle/detectPokemonIdent.ts index 3219f28f..77877104 100644 --- a/src/utils/battle/detectPokemonIdent.ts +++ b/src/utils/battle/detectPokemonIdent.ts @@ -2,14 +2,17 @@ import type { CalcdexPokemon } from '@showdex/redux/store'; export const detectPokemonIdent = ( pokemon: DeepPartial | DeepPartial | DeepPartial = {}, -): string => [ +): string => pokemon?.ident || [ + // 'p1', 'p2', etc. ('side' in pokemon ? pokemon.side?.sideid : null) || pokemon?.ident?.split?.(':')[0], - ('volatiles' in pokemon && pokemon.volatiles?.formechange?.[1]) - || pokemon?.speciesForme - || pokemon?.ident?.split?.(': ')[1] + + // speciesForme + pokemon?.speciesForme || pokemon?.details?.split?.(', ')?.[0] - || pokemon?.name, -].filter(Boolean).join(': ') + || pokemon?.searchid?.split?.('|')[1] + || pokemon?.ident?.split?.(': ')[1] + || pokemon?.name, // terrible cause it could be a nickname, but if we're here, oh well +].filter(Boolean).join(': ') // e.g., 'p1: Pikachu' || pokemon?.ident || pokemon?.searchid?.split?.('|')[0]; From 11650f572032b42aedd03148e5f94cad5740d3ed Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Sat, 20 Aug 2022 23:14:07 -0700 Subject: [PATCH 064/143] feat(utils): updated speciesForme detection --- src/utils/battle/detectSpeciesForme.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/utils/battle/detectSpeciesForme.ts b/src/utils/battle/detectSpeciesForme.ts index 934c39ac..44190e2d 100644 --- a/src/utils/battle/detectSpeciesForme.ts +++ b/src/utils/battle/detectSpeciesForme.ts @@ -1,16 +1,8 @@ import type { CalcdexPokemon } from '@showdex/redux/store'; -import { detectPokemonIdent } from './detectPokemonIdent'; - -/* eslint-disable arrow-body-style */ export const detectSpeciesForme = ( pokemon: DeepPartial | DeepPartial = {}, -): CalcdexPokemon['speciesForme'] => { - // if ('speciesForme' in (pokemon || {})) { - // return sanitizeSpeciesForme(pokemon.speciesForme); - // } - - return pokemon?.volatiles?.formechange?.[1] || - pokemon?.speciesForme || - detectPokemonIdent(pokemon)?.split?.(': ')?.[1]; -}; +): string => pokemon?.speciesForme // 'Zygarde-Complete' -- ideally we'd use this one + || pokemon?.details?.split?.(', ')[0] // 'Zygarde, L100, N' -> 'Zygarde' (normally just 'Zygarde' tho) + || pokemon?.searchid?.split?.('|')[1] // 'p1: Zygarde|Zygarde-Complete' -> 'Zygarde-Complete' + || pokemon?.ident?.split?.(': ')[1]; // 'p1: Zygarde' -> 'Zygarde' From 43b6fb20b005bd06db024d34d6a848b24ec712d0 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Sat, 20 Aug 2022 23:14:44 -0700 Subject: [PATCH 065/143] feat(utils): added detection of missing field conditions --- src/utils/battle/sanitizeField.ts | 3 +++ src/utils/battle/sanitizePlayerSide.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/utils/battle/sanitizeField.ts b/src/utils/battle/sanitizeField.ts index da28ed3f..a6c99791 100644 --- a/src/utils/battle/sanitizeField.ts +++ b/src/utils/battle/sanitizeField.ts @@ -31,6 +31,9 @@ export const sanitizeField = ( weather: WeatherMap?.[weather] ?? null, terrain: pseudoWeatherName ? PseudoWeatherMap?.[pseudoWeatherName] : null, + + isMagicRoom: pseudoWeatherMoveNames?.includes?.('Magic Room') ?? false, + isWonderRoom: pseudoWeatherMoveNames?.includes?.('Wonder Room') ?? false, isGravity: pseudoWeatherMoveNames?.includes?.('Gravity') ?? false, attackerSide: sanitizePlayerSide(p1, attackerIndex), diff --git a/src/utils/battle/sanitizePlayerSide.ts b/src/utils/battle/sanitizePlayerSide.ts index 8e69fca8..fd5f5167 100644 --- a/src/utils/battle/sanitizePlayerSide.ts +++ b/src/utils/battle/sanitizePlayerSide.ts @@ -26,6 +26,9 @@ export const sanitizePlayerSide = ( isAuroraVeil: sideConditionNames.includes('auroraveil'), // isBattery: null, // isPowerSpot: null, + isFirePledge: sideConditionNames.includes('firepledge'), + isGrassPledge: sideConditionNames.includes('grasspledge'), + isWaterPledge: sideConditionNames.includes('waterpledge'), // isSwitching: player?.active?.[0]?.ident === player?.active?.[activeIndex]?.ident ? 'out' : 'in', isSwitching: null, }; From f7640da3a9103f7f31abc4b3f8c9c29c625477f2 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Sat, 20 Aug 2022 23:15:16 -0700 Subject: [PATCH 066/143] feat(redux): updated types in calcdexSlice --- src/redux/store/calcdexSlice.ts | 139 +++++++++++++++++++++++++++++--- 1 file changed, 127 insertions(+), 12 deletions(-) diff --git a/src/redux/store/calcdexSlice.ts b/src/redux/store/calcdexSlice.ts index 39e3dd5d..077d6909 100644 --- a/src/redux/store/calcdexSlice.ts +++ b/src/redux/store/calcdexSlice.ts @@ -179,13 +179,16 @@ export interface CalcdexPokemon extends CalcdexLeanPokemon { /** * Whether the Pokemon object originates from the client or server. * - * * If the type if `Showdown.Pokemon`, then the Pokemon is *probably* from the client. - * * If the type is `Showdown.ServerPokemon`, then the Pokemon is from the server (duh). - * * ~~Used to determine which fields to overwrite when syncing.~~ - * - (See deprecation notice below.) + * * Used to determine whether the Pokemon's `hp` is a percentage or not. + * - If it's a percentage (`false`), then we'll need to calculate it from the `maxhp`, + * which may also need to be calculated. + * * `ServerPokemon` provides the actual values for `hp` and `maxhp`, + * while (client) `Pokemon` only provides a value range of `[0, 100]`, both inclusive. + * - Using the `ServerPokemon` allows for more accurate calculations, + * so if it's available, we'll use it. + * * This is primarily used in the `createSmogonPokemon()` utility. * * @default false - * @deprecated As of v0.1.3, although assigned, don't think this is used anymore. * @since 0.1.0 */ serverSourced?: boolean; @@ -193,6 +196,8 @@ export interface CalcdexPokemon extends CalcdexLeanPokemon { /** * Unsanitized version of `speciesForme`, primarily used for determining Z/Max/G-Max moves. * + * @deprecated As of v0.1.3, since we no longer need to sanitize the `speciesForme`, + * this is no longer needed. * @since 0.1.2 */ rawSpeciesForme?: string; @@ -230,11 +235,29 @@ export interface CalcdexPokemon extends CalcdexLeanPokemon { */ baseAbility?: AbilityName; + /** + * Whether the current `ability`/`dirtyAbility` is toggleable. + * + * * & the dev award for the best variable names goes to... + * * Used for showing the ability toggle button in `PokeInfo`. + * * Should be determined by whether the ability is in the list of `PokemonToggleAbilities`. + * - Special handling is required for the *Multiscale* ability, + * in which this value should be `false` if the Pokemon's HP is not 100%. + * + * @see `PokemonToggleAbilities` in `src/consts/abilities.ts`. + * @default false + * @since 0.1.3 + */ + abilityToggleable?: boolean; + /** * Some abilities are conditionally toggled, such as *Flash Fire*. * * * While we don't have to worry about those conditions, * we need to keep track of whether the ability is active. + * * Allows toggling by the user, but will sync with the battle state as the turn ends. + * * Internally, this value depends on `abilityToggleable`. + * - See `detectToggledAbility()` for implementation details. * * If the ability is not in `PokemonToggleAbilities` in `consts`, * this value will always be `true`, despite the default value being `false`. * @@ -341,7 +364,8 @@ export interface CalcdexPokemon extends CalcdexLeanPokemon { * * Typically contains moves set via user input or Smogon sets. * * Should not be synced with the current `app.curRoom.battle` state. * - Unless the originating Pokemon object is a `Showdown.ServerPokemon`. - * - In that instance, `serverSourced` should be `true`. + * - ~~In that instance, `serverSourced` should be `true`.~~ + * - Update (v0.1.3): `serverSourced` was unused, so it's now deprecated. * * @since 0.1.0 */ @@ -363,6 +387,8 @@ export interface CalcdexPokemon extends CalcdexLeanPokemon { * Whether the Pokemon is using Z/Max/G-Max moves. * * * Using the term *ultimate* (thanks Blizzard/Riot lmaoo) to cover the nomenclature for both Z (gen 7) and Max/G-Max (gen 8) moves. + * - Future me found the word I was looking for: *gen-agnostic*. + * - ... like in the sense of *platform-agnostic*. * * @since 0.1.2 */ @@ -385,13 +411,16 @@ export interface CalcdexPokemon extends CalcdexLeanPokemon { /** * Keeps track of user-modified boosts as to not modify the actual boosts from the `battle` state. * + * * Values for each stat (except for HP) are stored as boost **stages**, not as boost multipliers. + * - In other words, values should range `[-6, 6]`, both inclusive. + * * @default * ```ts * {} * ``` * @since 0.1.0 */ - dirtyBoosts?: Partial>; + dirtyBoosts?: Omit; /** * Base stats of the Pokemon based on its species. @@ -402,12 +431,41 @@ export interface CalcdexPokemon extends CalcdexLeanPokemon { * ``` * @since 0.1.0 */ - baseStats?: Partial; + baseStats?: Showdown.StatsTable; + + /** + * Server-reported stats of the Pokemon. + * + * * Only provided if the Pokemon belongs to the player. + * - Spectators won't receive this information (they only receive client `Showdown.Pokemon` objects). + * * HP value is derived from the `maxhp` of the `Showdown.ServerPokemon` object. + * * EVs/IVs/nature are factored in, but not items or abilities. + * - Server doesn't report the actual EVs/IVs/nature, so we get to figure them out ourselves! + * + * @default + * ```ts + * {} + * ``` + * @since 0.1.3 + */ + serverStats?: Showdown.StatsTable; /** - * Calculated stats of the Pokemon based on its current properties. + * Calculated stats of the Pokemon after its EV/IV/nature spread is applied. * * * This does not factor in items, abilities, or field conditions. + * - Final stats are not stored here since it requires additional information + * from field conditions and potentially other ally and opponent Pokemon. + * - See `calcPokemonFinalStats()` for more information. + * * As of v0.1.3, value is set in `syncPokemon()` when `serverPokemon` is provided and + * in `applyPreset()` of `PokeInfo` when a `preset` is applied. + * - Additionally, this has been renamed from `calculatedStats` (pre-v1.0.3) to + * `spreadStats` to avoid confusion between this and existing stat properties. + * - Furthermore, `guessServerSpread()` internally uses a local `calculatedStats` object + * that's unrelated to this one, adding to the confusion. + * * Since the user is free to change the EVs/IVs/nature, this value should not be synced with + * the provided `stats` in the corresponding `ServerPokemon`, if applicable. + * - Server-reported `stats` should be synced with the `serverStats` instead. * * @default * ```ts @@ -415,7 +473,7 @@ export interface CalcdexPokemon extends CalcdexLeanPokemon { * ``` * @since 0.1.0 */ - calculatedStats?: Partial; + spreadStats?: Showdown.StatsTable; /** * Whether to calculate move damages as critical hits. @@ -560,8 +618,65 @@ export interface CalcdexPlayer extends CalcdexLeanSide { pokemon?: CalcdexPokemon[]; } -export type CalcdexPlayerSide = SmogonState.Side; -export type CalcdexBattleField = SmogonState.Field; +/** + * Think someone at `@smogon/calc` forgot to include these additional field conditions + * in the `State.Field` (but it exists in the `Field` class... huh). + * + * * For whatever reason, `isGravity` exists on both `State.Field` and `Field`. + * * Checking the source code for the `Field` class (see link below), + * the constructor accepts these missing properties. + * + * @see https://github.com/smogon/damage-calc/blob/master/calc/src/field.ts#L21-L26 + * @since 0.1.3 + */ +export interface CalcdexBattleField extends SmogonState.Field { + isMagicRoom?: boolean; + isWonderRoom?: boolean; + isAuraBreak?: boolean; + isFairyAura?: boolean; + isDarkAura?: boolean; + attackerSide: CalcdexPlayerSide; + defenderSide: CalcdexPlayerSide; +} + +/** + * As is the case with `CalcdexBattleField`, this adds the missing properties that exist + * in `Side`, but not `State.Side`. + * + * * Additional properties that will be unused by the `Side` constructor are included + * as they may be used in Pokemon stat calculations. + * + * @see https://github.com/smogon/damage-calc/blob/master/calc/src/field.ts#L84-L102 + * @since 0.1.3 + */ +export interface CalcdexPlayerSide extends SmogonState.Side { + isProtected?: boolean; + isSeeded?: boolean; + isFriendGuard?: boolean; + isBattery?: boolean; + isPowerSpot?: boolean; + + /** + * Not used by the calc, but recorded for Pokemon stat calculations. + * + * @since 0.1.3 + */ + isFirePledge?: boolean; + + /** + * Not used by the calc, but recorded for Pokemon stat calculations. + * + * @since 0.1.3 + */ + isGrassPledge?: boolean; + + /** + * Not used by the calc, but recorded for Pokemon stat calculations. + * + * @since 0.1.3 + */ + isWaterPledge?: boolean; +} /** * Key of a given player. From b06b0554630d55d9133df0cea9ca14ca3965b604 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Sat, 20 Aug 2022 23:17:05 -0700 Subject: [PATCH 067/143] feat(utils): updated Pokemon sanitization --- src/utils/battle/sanitizePokemon.ts | 68 +++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 19 deletions(-) diff --git a/src/utils/battle/sanitizePokemon.ts b/src/utils/battle/sanitizePokemon.ts index d1bd3458..be7867f7 100644 --- a/src/utils/battle/sanitizePokemon.ts +++ b/src/utils/battle/sanitizePokemon.ts @@ -1,11 +1,41 @@ import { PokemonNatures } from '@showdex/consts'; -import { calcPokemonCalcdexId, calcPokemonCalcdexNonce } from '@showdex/utils/calc'; +import { calcPokemonCalcdexId } from '@showdex/utils/calc'; import type { AbilityName, ItemName, MoveName } from '@pkmn/data'; import type { CalcdexPokemon } from '@showdex/redux/store'; import { detectPokemonIdent } from './detectPokemonIdent'; import { detectSpeciesForme } from './detectSpeciesForme'; import { detectToggledAbility } from './detectToggledAbility'; -// import { sanitizeSpeciesForme } from './sanitizeSpeciesForme'; +import { toggleableAbility } from './toggleableAbility'; + +/** + * Pokemon `volatiles` require special love and attention before they get Redux'd. + * + * * Ditto + * - ...and Mew, I guess + * * hnnnnnnnnnnnnnnnnnnnnnnnng + * * Separated from `sanitizePokemon()` cause I'm probably using it elsewhere. + * + * @since 0.1.3 + */ +export const sanitizePokemonVolatiles = ( + pokemon: DeepPartial | DeepPartial = {}, +): CalcdexPokemon['volatiles'] => Object.entries(pokemon?.volatiles || {}).reduce((volatiles, [id, volatile]) => { + const [, value] = volatile || []; + + // we're gunna replace the Pokemon object w/ its ident if it's a transform volatile + const transformed = id === 'transform' && + typeof ( value?.[1])?.ident === 'string'; + + if (transformed || !value?.[1] || ['string', 'number'].includes(typeof value[1])) { + volatiles[id] = transformed ? [ + id, // value[0] is also the id + ( value[1]).ident, + ...value.slice(2), + ] : volatile; + } + + return volatiles; +}, {}); /** * Essentially converts a `Showdown.Pokemon` into our custom `CalcdexPokemon`. @@ -24,13 +54,13 @@ export const sanitizePokemon = ( ): CalcdexPokemon => { const sanitizedPokemon: CalcdexPokemon = { calcdexId: ('calcdexId' in pokemon && pokemon.calcdexId) || null, - calcdexNonce: ('calcdexNonce' in pokemon && pokemon.calcdexNonce) || null, + // calcdexNonce: ('calcdexNonce' in pokemon && pokemon.calcdexNonce) || null, slot: pokemon?.slot ?? null, // could be 0, so don't use logical OR here ident: detectPokemonIdent(pokemon), searchid: pokemon?.searchid, speciesForme: detectSpeciesForme(pokemon), - rawSpeciesForme: pokemon?.speciesForme, + // rawSpeciesForme: pokemon?.speciesForme, name: pokemon?.name, details: pokemon?.details, @@ -44,7 +74,7 @@ export const sanitizePokemon = ( ability: pokemon?.ability || ('abilities' in pokemon && pokemon.abilities[0]) || null, dirtyAbility: ('dirtyAbility' in pokemon && pokemon.dirtyAbility) || null, - abilityToggled: 'abilityToggled' in pokemon ? pokemon.abilityToggled : detectToggledAbility(pokemon), + // abilityToggled: 'abilityToggled' in pokemon ? pokemon.abilityToggled : detectToggledAbility(pokemon), baseAbility: pokemon?.baseAbility, abilities: ('abilities' in pokemon && pokemon.abilities) || [], altAbilities: ('altAbilities' in pokemon && pokemon.altAbilities) || [], @@ -99,16 +129,8 @@ export const sanitizePokemon = ( }, // only deep-copy non-object volatiles - // (particularly Ditto's `transformed` volatile, which references an existing Pokemon object as its value) - volatiles: Object.entries(pokemon?.volatiles || {}).reduce((volatiles, [id, volatile]) => { - const [, value] = volatile || []; - - if (!value?.[1] || ['string', 'number'].includes(typeof value[1])) { - volatiles[id] = JSON.parse(JSON.stringify(volatile)); - } - - return volatiles; - }, {}), + // (particularly Ditto's 'transform' volatile, which references an existing Pokemon object as its value) + volatiles: sanitizePokemonVolatiles(pokemon), turnstatuses: pokemon?.turnstatuses, toxicCounter: pokemon?.statusData?.toxicTurns, @@ -121,11 +143,13 @@ export const sanitizePokemon = ( altMoves: ('altMoves' in pokemon && pokemon.altMoves) || [], useUltimateMoves: ('useUltimateMoves' in pokemon && pokemon.useUltimateMoves) || false, lastMove: pokemon?.lastMove, - moveTrack: Array.isArray(pokemon?.moveTrack) ? + + moveTrack: Array.isArray(pokemon?.moveTrack) // since pokemon.moveTrack is an array of arrays, // we don't want to reference the original inner array elements - JSON.parse(JSON.stringify(pokemon?.moveTrack)) : - [], + ? structuredClone(pokemon.moveTrack) + : [], + moveState: { revealed: ('moveState' in pokemon && pokemon.moveState?.revealed) || [], learnset: ('moveState' in pokemon && pokemon.moveState?.learnset) || [], @@ -139,6 +163,12 @@ export const sanitizePokemon = ( autoPreset: 'autoPreset' in pokemon ? pokemon.autoPreset : true, }; + // abilityToggleable is mainly used for UI, hence why there are two of + // what seems to be essentially the same thing + // (but note that abilityToggled stores the current toggle state) + sanitizedPokemon.abilityToggleable = toggleableAbility(sanitizedPokemon); + sanitizedPokemon.abilityToggled = detectToggledAbility(sanitizedPokemon); + // fill in additional info if the Dex global is available (should be) if (typeof Dex?.species?.get === 'function') { const species = Dex.species.get(sanitizedPokemon.speciesForme); @@ -171,7 +201,7 @@ export const sanitizePokemon = ( sanitizedPokemon.calcdexId = calcPokemonCalcdexId(sanitizedPokemon); } - sanitizedPokemon.calcdexNonce = calcPokemonCalcdexNonce(sanitizedPokemon); + // sanitizedPokemon.calcdexNonce = calcPokemonCalcdexNonce(sanitizedPokemon); return sanitizedPokemon; }; From 6d4dd18cbc171e42dffa73057ba37f50954e9fd5 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Sat, 20 Aug 2022 23:17:28 -0700 Subject: [PATCH 068/143] feat(utils): updated Pokemon syncing --- src/utils/battle/syncPokemon.ts | 320 ++++++++++++++++---------------- 1 file changed, 155 insertions(+), 165 deletions(-) diff --git a/src/utils/battle/syncPokemon.ts b/src/utils/battle/syncPokemon.ts index 311a7383..7915569e 100644 --- a/src/utils/battle/syncPokemon.ts +++ b/src/utils/battle/syncPokemon.ts @@ -1,68 +1,21 @@ import { PokemonBoostNames } from '@showdex/consts'; -import { calcPokemonStats, calcPresetCalcdexId, guessServerSpread } from '@showdex/utils/calc'; -import { logger } from '@showdex/utils/debug'; +import { + calcPokemonSpreadStats, + calcPresetCalcdexId, + guessServerSpread, +} from '@showdex/utils/calc'; +import { env } from '@showdex/utils/core'; +// import { logger } from '@showdex/utils/debug'; +import type { Generation, GenerationNum } from '@pkmn/data'; import type { - // AbilityName, - Generation, - // ItemName, - // MoveName, -} from '@pkmn/data'; -import type { - CalcdexMoveState, + // CalcdexMoveState, CalcdexPokemon, CalcdexPokemonPreset, } from '@showdex/redux/store'; -import { sanitizePokemon } from './sanitizePokemon'; - -const l = logger('@showdex/utils/battle/syncPokemon'); - -export const syncPokemonBoosts = ( - pokemon: CalcdexPokemon, - clientPokemon: DeepPartial, -): CalcdexPokemon['boosts'] => { - const newPokemon: CalcdexPokemon = { ...pokemon }; - - l.debug( - 'syncPokemonBoosts()', - '\n', 'pokemon', pokemon, - '\n', 'clientPokemon', clientPokemon, - ); - - const boosts = PokemonBoostNames.reduce((prev, stat) => { - const currentValue = prev[stat]; - const value = clientPokemon?.boosts?.[stat] || 0; - - // l.debug( - // 'syncPokemonBoosts()', - // '\n', 'comparing stat', stat, 'currentValue', currentValue, 'with value', value, - // '\n', 'newPokemon', newPokemon?.ident, newPokemon, - // ); +import { sanitizePokemon, sanitizePokemonVolatiles } from './sanitizePokemon'; +import { detectToggledAbility } from './detectToggledAbility'; - // l.debug(pokemon.ident, 'comparing value', value, 'and currentValue', currentValue, 'for stat', stat); - - if (value === currentValue) { - return prev; - } - - prev[stat] = value; - - return prev; - }, { - atk: newPokemon?.boosts?.atk || 0, - def: newPokemon?.boosts?.def || 0, - spa: newPokemon?.boosts?.spa || 0, - spd: newPokemon?.boosts?.spd || 0, - spe: newPokemon?.boosts?.spe || 0, - }); - - l.debug( - 'syncPokemonBoosts() -> return boosts', - '\n', 'boosts', boosts, - '\n', 'newPokemon', newPokemon?.ident, newPokemon, - ); - - return boosts; -}; +// const l = logger('@showdex/utils/battle/syncPokemon'); export const syncPokemon = ( pokemon: CalcdexPokemon, @@ -71,46 +24,61 @@ export const syncPokemon = ( dex?: Generation, format?: string, ): CalcdexPokemon => { - const newPokemon: CalcdexPokemon = { ...pokemon }; + // final synced Pokemon that will be returned at the end + const syncedPokemon = structuredClone(pokemon) || {}; - (<(keyof Showdown.Pokemon)[]> [ + // you should not be looping through any special CalcdexPokemon-specific properties here! + (<(keyof NonFunctionProperties)[]> [ 'name', - 'speciesForme', - // 'rawSpeciesForme', 'hp', 'maxhp', 'status', 'statusData', 'ability', 'baseAbility', - // 'nature', 'item', 'itemEffect', 'prevItem', 'prevItemEffect', 'moveTrack', 'volatiles', - // 'abilityToggled', // should be after volatiles 'turnstatuses', 'boosts', ]).forEach((key) => { - const currentValue = newPokemon[ key]; // `newPokemon` is the final synced Pokemon that will be returned at the end - let value = clientPokemon?.[key]; // `clientPokemon` is what was changed and may not be a full Pokemon object + const prevValue = syncedPokemon[ key]; + let value = clientPokemon?.[key]; if (value === undefined) { return; } switch (key) { + case 'hp': + case 'maxhp': { + // note: returning at any point here will skip syncing the `value` from the + // Showdown.Pokemon (i.e., clientPokemon) to the CalcdexPokemon (i.e., syncedPokemon) + // (but only for the current `key` of the iteration, of course) + if (typeof serverPokemon?.hp === 'number' && typeof serverPokemon.maxhp === 'number') { + return; + } + + // note: breaking will continue the sync operation + // (which in this case, if a serverPokemon wasn't provided, we'll use the hp/maxhp from the clientPokemon) + break; + } + case 'ability': { if (!value) { return; } - if (value === newPokemon.dirtyAbility) { - newPokemon.dirtyAbility = null; + if (value === syncedPokemon.dirtyAbility) { + syncedPokemon.dirtyAbility = null; } + // update the abilityToggled state (always false if not applicable) + syncedPokemon.abilityToggled = detectToggledAbility(clientPokemon); + break; } @@ -124,8 +92,8 @@ export const syncPokemon = ( // clear the dirtyItem if it's what the Pokemon actually has // (otherwise, if the item hasn't been revealed yet, `value` would be falsy, // but that's ok cause we have dirtyItem, i.e., no worries about clearing the user's input) - if (value === newPokemon.dirtyItem) { - newPokemon.dirtyItem = null; + if (value === syncedPokemon.dirtyItem) { + syncedPokemon.dirtyItem = null; } break; @@ -135,42 +103,38 @@ export const syncPokemon = ( // check if the item was knocked-off and is the same as dirtyItem // if so, clear the dirtyItem // (note that `value` here is prevItem, NOT item!) - if (clientPokemon?.prevItemEffect === 'knocked off' && value === newPokemon.dirtyItem) { - newPokemon.dirtyItem = null; + if (clientPokemon?.prevItemEffect === 'knocked off' && value === syncedPokemon.dirtyItem) { + syncedPokemon.dirtyItem = null; } break; } - case 'speciesForme': { - if (clientPokemon?.volatiles?.formechange?.[1]) { - [, value] = clientPokemon.volatiles.formechange; - } - - /** @todo */ - // value = sanitizeSpeciesForme( value); - - break; - } - - // case 'rawSpeciesForme': { - // value = clientPokemon?.speciesForme ?? newPokemon?.rawSpeciesForme ?? newPokemon?.speciesForme; - // - // if (!value) { - // return; - // } - // - // break; - // } - case 'boosts': { - value = syncPokemonBoosts(newPokemon, clientPokemon); + value = PokemonBoostNames.reduce((prev, stat) => { + const prevBoost = prev[stat]; + const boost = clientPokemon?.boosts?.[stat] || 0; + + if (boost !== prevBoost) { + prev[stat] = boost; + } + + return prev; + }, { + atk: syncedPokemon.boosts?.atk || 0, + def: syncedPokemon.boosts?.def || 0, + spa: syncedPokemon.boosts?.spa || 0, + spd: syncedPokemon.boosts?.spd || 0, + spe: syncedPokemon.boosts?.spe || 0, + }); break; } case 'moves': { - if (!( value)?.length) { + const moves = value; + + if (!moves?.length) { return; } @@ -178,71 +142,72 @@ export const syncPokemon = ( } case 'moveTrack': { - // l.debug('clientPokemon.moveTrack', clientPokemon?.moveTrack); + const moveTrack = value; - if (clientPokemon?.moveTrack?.length) { - newPokemon.moveState = { - ...newPokemon.moveState, - revealed: clientPokemon.moveTrack.map((track) => track?.[0]), + if (moveTrack?.length) { + syncedPokemon.moveState = { + ...syncedPokemon.moveState, + + // filter out any Z/Max moves from the revealed list + revealed: moveTrack.map((track) => track?.[0]).filter((m) => { + const move = dex?.moves?.get?.(m); + + return !!move?.name && !move?.isZ && !move?.isMax; + }), }; - // l.debug('value of type CalcdexMoveState set to', newPokemon.moveState); + // l.debug('value of type CalcdexMoveState set to', syncedPokemon.moveState); } break; } - // case 'presets': { - // if (!Array.isArray(value) || !value.length) { - // value = currentValue; - // } - // - // break; - // } - - // case 'abilityToggled': { - // value = detectToggledAbility(clientPokemon); - // - // break; - // } - - default: break; + case 'volatiles': { + const volatiles = value; + + // sync Pokemon's dynamax state + syncedPokemon.useUltimateMoves = 'dynamax' in volatiles; + + /** + * @todo handle Ditto transformations here + */ + + // sanitizing to make sure a transformed ditto/mew doesn't crash the extension lol + value = sanitizePokemonVolatiles(clientPokemon); + + break; + } + + default: { + break; + } } - /** @todo this line breaks when Ditto transforms since `volatiles.transformed[1]` is `Showdown.Pokemon` (NOT `string`) */ - if (JSON.stringify(value) === JSON.stringify(currentValue)) { // kekw + if (JSON.stringify(value) === JSON.stringify(prevValue)) { // kekw return; } - /** @see https://github.com/microsoft/TypeScript/issues/31663#issuecomment-518603958 */ - (newPokemon as Record)[key] = JSON.parse(JSON.stringify(value)); + syncedPokemon[key] = structuredClone(value); }); - // only using sanitizePokemon() to get some values back - const sanitizedPokemon = sanitizePokemon(newPokemon); - - // update some info if the Pokemon's speciesForme changed - // (since moveState requires async, we update that in syncBattle()) - if (pokemon.speciesForme !== newPokemon.speciesForme) { - newPokemon.baseStats = { ...sanitizedPokemon.baseStats }; - newPokemon.types = sanitizedPokemon.types; - newPokemon.ability = sanitizedPokemon.ability; - newPokemon.dirtyAbility = null; - newPokemon.abilities = sanitizedPokemon.abilities; - } - - newPokemon.abilityToggled = sanitizedPokemon.abilityToggled; - // fill in some additional fields if the serverPokemon was provided if (serverPokemon) { - newPokemon.serverSourced = true; + // should always be the case, idk why it shouldn't be (but you know we gotta check) + if (typeof serverPokemon.hp === 'number' && typeof serverPokemon.maxhp === 'number') { + // serverSourced is used primarily as a flag to distinguish `hp` as the actual value or as a percentage + // (but since this conditional should always succeed in theory, should be ok to use to distinguish other properties) + syncedPokemon.serverSourced = true; + + syncedPokemon.hp = serverPokemon.hp; + syncedPokemon.maxhp = serverPokemon.maxhp; + } if (serverPokemon.ability) { const dexAbility = dex.abilities.get(serverPokemon.ability); if (dexAbility?.name) { - newPokemon.ability = dexAbility.name; - newPokemon.dirtyAbility = null; + syncedPokemon.ability = dexAbility.name; + syncedPokemon.dirtyAbility = null; } } @@ -250,34 +215,42 @@ export const syncPokemon = ( const dexItem = dex.items.get(serverPokemon.item); if (dexItem?.name) { - newPokemon.item = dexItem.name; - newPokemon.dirtyItem = null; + syncedPokemon.item = dexItem.name; + syncedPokemon.dirtyItem = null; } } - // build a preset around the serverPokemon + // copy the server stats for more accurate final stats calculations + syncedPokemon.serverStats = { + hp: serverPokemon.maxhp, + ...serverPokemon.stats, + }; + + // since the server doesn't send us the Pokemon's EVs/IVs/nature, we gotta find it ourselves + // (note that this function doesn't pull from syncedPokemon.serverStats, but rather serverPokemon.stats) const guessedSpread = guessServerSpread( dex, - newPokemon, - serverPokemon, + syncedPokemon, + // serverPokemon, // since we have serverStats now, no need for this lol format?.includes('random') ? 'Hardy' : undefined, ); + // build a preset around the serverPokemon const serverPreset: CalcdexPokemonPreset = { name: 'Yours', - gen: dex.num, + gen: dex.num || env.int('calcdex-default-gen'), format, - speciesForme: newPokemon.speciesForme || serverPokemon.speciesForme, - level: newPokemon.level || serverPokemon.level, - gender: newPokemon.gender || serverPokemon.gender || null, - ability: newPokemon.ability, - item: newPokemon.item, + speciesForme: syncedPokemon.speciesForme || serverPokemon.speciesForme, + level: syncedPokemon.level || serverPokemon.level, + gender: syncedPokemon.gender || serverPokemon.gender || null, + ability: syncedPokemon.ability, + item: syncedPokemon.item, ...guessedSpread, }; - newPokemon.nature = serverPreset.nature; - newPokemon.ivs = { ...serverPreset.ivs }; - newPokemon.evs = { ...serverPreset.evs }; + syncedPokemon.nature = serverPreset.nature; + syncedPokemon.ivs = { ...serverPreset.ivs }; + syncedPokemon.evs = { ...serverPreset.evs }; // need to do some special processing for moves // e.g., serverPokemon.moves = ['calmmind', 'moonblast', 'flamethrower', 'thunderbolt'] @@ -293,38 +266,55 @@ export const syncPokemon = ( return dexMove.name; }).filter(Boolean); - newPokemon.moves = [...serverPreset.moves]; + syncedPokemon.moves = [...serverPreset.moves]; } // calculate the stats with the EVs/IVs from the server preset // (note: same thing happens in applyPreset() in PokeInfo since the EVs/IVs from the preset are now available) if (typeof dex?.stats?.calc === 'function') { - newPokemon.calculatedStats = calcPokemonStats(dex, newPokemon); + syncedPokemon.spreadStats = calcPokemonSpreadStats(dex, syncedPokemon); } serverPreset.calcdexId = calcPresetCalcdexId(serverPreset); - const serverPresetIndex = newPokemon.presets.findIndex((p) => p.calcdexId === serverPreset.calcdexId); + const serverPresetIndex = syncedPokemon.presets + .findIndex((p) => p.calcdexId === serverPreset.calcdexId); if (serverPresetIndex > -1) { - newPokemon.presets[serverPresetIndex] = serverPreset; + syncedPokemon.presets[serverPresetIndex] = serverPreset; } else { - newPokemon.presets.unshift(serverPreset); + syncedPokemon.presets.unshift(serverPreset); } // disabling autoPreset since we already set the preset here // (also tells PokeInfo not to apply the first preset) - newPokemon.preset = serverPreset.calcdexId; - newPokemon.autoPreset = false; + syncedPokemon.preset = serverPreset.calcdexId; + syncedPokemon.autoPreset = false; } - // const calcdexId = calcPokemonCalcdexId(newPokemon); + // only using sanitizePokemon() to get some values back + const sanitizedPokemon = sanitizePokemon(syncedPokemon); + + // update some info if the Pokemon's speciesForme changed + // (since moveState requires async, we update that in syncBattle()) + // if (pokemon.speciesForme !== syncedPokemon.speciesForme) { + // syncedPokemon.baseStats = { ...sanitizedPokemon.baseStats }; + // syncedPokemon.types = sanitizedPokemon.types; + // syncedPokemon.ability = sanitizedPokemon.ability; + // syncedPokemon.dirtyAbility = null; + // syncedPokemon.abilities = sanitizedPokemon.abilities; + // } + + syncedPokemon.abilityToggleable = sanitizedPokemon.abilityToggleable; + syncedPokemon.abilityToggled = sanitizedPokemon.abilityToggled; + + // const calcdexId = calcPokemonCalcdexId(syncedPokemon); - // if (!newPokemon?.calcdexId || newPokemon.calcdexId !== calcdexId) { - // newPokemon.calcdexId = calcdexId; + // if (!syncedPokemon?.calcdexId || syncedPokemon.calcdexId !== calcdexId) { + // syncedPokemon.calcdexId = calcdexId; // } - // newPokemon.calcdexNonce = sanitizedPokemon.calcdexNonce; + // syncedPokemon.calcdexNonce = sanitizedPokemon.calcdexNonce; - return newPokemon; + return syncedPokemon; }; From 5a95c92782d210ca3e6736d548310909c81c6a3c Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Sat, 20 Aug 2022 23:18:12 -0700 Subject: [PATCH 069/143] refac(utils): changed spreadStats in detectStatBoostDelta --- src/utils/battle/detectStatBoostDelta.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/utils/battle/detectStatBoostDelta.ts b/src/utils/battle/detectStatBoostDelta.ts index 7592259f..b030b014 100644 --- a/src/utils/battle/detectStatBoostDelta.ts +++ b/src/utils/battle/detectStatBoostDelta.ts @@ -7,8 +7,9 @@ export type PokemonStatBoostDelta = /** * Determines the direction of the delta of the Pokemon's stat boost. * - * * In other words, checks whether the Pokemon's current stats are higher or lower than its base stats. - * * In cases where it's neither (i.e., no boosts were applied to the passed-in `stat`), `null` will be returned. + * * In other words, checks whether the Pokemon's ~~current~~ final stats are higher or lower than its ~~base~~ spread stats. + * * In cases where it's neither (i.e., spread stat and final stat are equal), `null` will be returned. + * * Typically used for applying styling to the UI to indicate a boosted or reduced stat value. * * @since 0.1.2 */ @@ -55,14 +56,14 @@ export const detectStatBoostDelta = ( // const boost = pokemon?.dirtyBoosts?.[stat] ?? pokemon?.boosts?.[stat] ?? 0; - const calculatedStat = pokemon?.calculatedStats?.[stat] ?? 0; + const spreadStat = pokemon?.spreadStats?.[stat] ?? 0; const finalStat = finalStats?.[stat] ?? 0; - if (finalStat > calculatedStat) { + if (finalStat > spreadStat) { return 'positive'; } - if (finalStat < calculatedStat) { + if (finalStat < spreadStat) { return 'negative'; } From f9c430516c3acd79733e4c5431816bc7a05efea2 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Sat, 20 Aug 2022 23:18:37 -0700 Subject: [PATCH 070/143] docs(utils): removed note on speciesForme in getMaxMove --- src/utils/battle/getMaxMove.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/utils/battle/getMaxMove.ts b/src/utils/battle/getMaxMove.ts index c30d1186..cda4c685 100644 --- a/src/utils/battle/getMaxMove.ts +++ b/src/utils/battle/getMaxMove.ts @@ -29,8 +29,6 @@ const l = logger('@showdex/utils/app/getMaxMove'); * Returns the corresponding Max/G-Max move for a given move. * * * This requires the `'-Gmax'` suffix in the passed-in `speciesForme` to distinguish between Max and G-Max moves! - * - i.e., do not sanitize via `sanitizeSpeciesForme()` for the `speciesForme` argument. - * * If the Pokemon is a `CalcdexPokemon`, use the `rawSpeciesForme` over `speciesForme` instead. * - e.g., `'Alcremie-Gmax'` should be passed in for the `speciesForme` argument, not just `'Alcremie'`. * * @see https://github.com/smogon/damage-calc/blob/bdf9e8c39fec7670ed0ce64e1fb58d1a4dc83b73/calc/src/move.ts#L242 From 9719356f6ff1afd466f8ed7db9e63b191cc27bde Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Sat, 20 Aug 2022 23:19:04 -0700 Subject: [PATCH 071/143] feat(utils): added toggleableAbility battle util --- src/utils/battle/index.ts | 1 + src/utils/battle/toggleableAbility.ts | 32 +++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 src/utils/battle/toggleableAbility.ts diff --git a/src/utils/battle/index.ts b/src/utils/battle/index.ts index 0e681deb..10dab94d 100644 --- a/src/utils/battle/index.ts +++ b/src/utils/battle/index.ts @@ -12,3 +12,4 @@ export * from './sanitizePlayerSide'; export * from './sanitizePokemon'; export * from './syncField'; export * from './syncPokemon'; +export * from './toggleableAbility'; diff --git a/src/utils/battle/toggleableAbility.ts b/src/utils/battle/toggleableAbility.ts new file mode 100644 index 00000000..7798fa9a --- /dev/null +++ b/src/utils/battle/toggleableAbility.ts @@ -0,0 +1,32 @@ +import { PokemonToggleAbilities } from '@showdex/consts'; +import { formatId } from '@showdex/utils/app'; +import { calcPokemonHp } from '@showdex/utils/calc'; +import type { AbilityName } from '@pkmn/data'; +import type { CalcdexPokemon } from '@showdex/redux/store'; + +/** + * Determines the value of the `abilityToggleable` property in a `CalcdexPokemon`. + * + * * 10/10 name, I know. + * + * @see `CalcdexPokemon['abilityToggleable']` in `src/redux/store/calcdexSlice.ts` + * @since 0.1.3 + */ +export const toggleableAbility = ( + pokemon: DeepPartial | DeepPartial = {}, +): boolean => { + const ability = 'dirtyAbility' in pokemon ? + pokemon.dirtyAbility || pokemon.ability : + pokemon?.ability; + + if (!ability) { + return false; + } + + // Multiscale should only be toggleable if the Pokemon has 100% HP + if (formatId(ability) === 'multiscale') { + return calcPokemonHp(pokemon) === 1; + } + + return PokemonToggleAbilities.includes(ability); +}; From a685add66dd32e61371dff2e8ef51544a2784311 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Sat, 20 Aug 2022 23:19:32 -0700 Subject: [PATCH 072/143] refac(utils): added toggleableAbility to detectToggledAbility --- src/utils/battle/detectToggledAbility.ts | 25 +++++------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/src/utils/battle/detectToggledAbility.ts b/src/utils/battle/detectToggledAbility.ts index 3c244212..d8f0bb83 100644 --- a/src/utils/battle/detectToggledAbility.ts +++ b/src/utils/battle/detectToggledAbility.ts @@ -1,7 +1,6 @@ -import { PokemonToggleAbilities } from '@showdex/consts'; -import { calcPokemonHp } from '@showdex/utils/calc'; -import type { AbilityName } from '@pkmn/data'; +import { formatId } from '@showdex/utils/app'; import type { CalcdexPokemon } from '@showdex/redux/store'; +import { toggleableAbility } from './toggleableAbility'; /** * Determines whether the Pokemon's toggleable ability is active (if applicable). @@ -26,20 +25,6 @@ import type { CalcdexPokemon } from '@showdex/redux/store'; */ export const detectToggledAbility = ( pokemon: DeepPartial | DeepPartial = {}, -): boolean => { - const ability = pokemon?.ability; - const hasMultiscale = ability?.toLowerCase?.() === 'multiscale'; - const toggleAbility = !hasMultiscale && PokemonToggleAbilities.includes( ability); - - if (hasMultiscale) { - return calcPokemonHp(pokemon) === 1; - } - - if (toggleAbility) { - return Object.keys(pokemon?.volatiles ?? {}).includes( - ability?.replace?.(/\s/g, '').toLowerCase(), - ); - } - - return false; -}; +): boolean => toggleableAbility(pokemon) && Object.keys(pokemon.volatiles || {}).includes( + formatId(('dirtyAbility' in pokemon && pokemon.dirtyAbility) || pokemon.ability), +); From 9db47dfb93976f53bbedd3fa7492f44c0cebc3c3 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Sat, 20 Aug 2022 23:19:58 -0700 Subject: [PATCH 073/143] refac(utils): renamed calcPokemonSpreadStats --- src/utils/calc/calcPokemonSpreadStats.ts | 40 ++++++++++++++++++++++++ src/utils/calc/calcPokemonStats.ts | 28 ----------------- src/utils/calc/index.ts | 2 +- 3 files changed, 41 insertions(+), 29 deletions(-) create mode 100644 src/utils/calc/calcPokemonSpreadStats.ts delete mode 100644 src/utils/calc/calcPokemonStats.ts diff --git a/src/utils/calc/calcPokemonSpreadStats.ts b/src/utils/calc/calcPokemonSpreadStats.ts new file mode 100644 index 00000000..8ae034bd --- /dev/null +++ b/src/utils/calc/calcPokemonSpreadStats.ts @@ -0,0 +1,40 @@ +import { PokemonInitialStats, PokemonStatNames } from '@showdex/consts'; +import type { Generation } from '@pkmn/data'; +import type { CalcdexPokemon } from '@showdex/redux/store'; + +/** + * Calculates the stats of a Pokemon based on its applied EV/IV/nature spread. + * + * * Assumes that `baseStats` are already pre-populated, hence why `pokemon` does not + * accept type `Showdown.Pokemon`. + * - Returns all `0`s for each stat if `dex.stats.calc()` and/or `baseStats` are not provided. + * * Default EV of `0` and IV of `31` are applied if the corresponding EV/IV for a stat + * does not exist in the provided `pokemon`. + * - Not as important, but worth mentioning, if the Pokemon has no `level` defined (or is falsy, like `0`), + * the default level of `100` will apply. + * * As of v0.1.3, this has been renamed from `calcPokemonStats()` to `calcPokemonSpreadStats()`, + * to better indicate what `CalcdexPokemon` property this is meant to populate (i.e., `spreadStats`). + * + * @since 0.1.0 + */ +export const calcPokemonSpreadStats = ( + dex: Generation, + pokemon: DeepPartial, +): Partial => { + if (typeof dex?.stats?.calc !== 'function' || !Object.keys(pokemon?.baseStats || {}).length) { + return { ...PokemonInitialStats }; + } + + return PokemonStatNames.reduce((prev, stat) => { + prev[stat] = dex.stats.calc( + stat, + pokemon.baseStats[stat], + pokemon.ivs?.[stat] ?? 31, + pokemon.evs?.[stat] ?? 0, + pokemon.level || 100, + dex.natures.get(pokemon.nature), + ); + + return prev; + }, { ...PokemonInitialStats }); +}; diff --git a/src/utils/calc/calcPokemonStats.ts b/src/utils/calc/calcPokemonStats.ts deleted file mode 100644 index f58066a9..00000000 --- a/src/utils/calc/calcPokemonStats.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { PokemonStatNames } from '@showdex/consts'; -import type { Generation } from '@pkmn/data'; -import type { CalcdexPokemon } from '@showdex/redux/store'; - -const initialStats: Showdown.StatsTable = { - hp: 0, - atk: 0, - def: 0, - spa: 0, - spd: 0, - spe: 0, -}; - -export const calcPokemonStats = ( - dex: Generation, - pokemon: DeepPartial, -): Partial => PokemonStatNames.reduce((prev, stat) => { - prev[stat] = dex.stats.calc( - stat, - pokemon.baseStats[stat], - pokemon.ivs?.[stat] ?? 31, - pokemon.evs?.[stat] ?? 0, - pokemon.level || 100, - dex.natures.get(pokemon.nature), - ); - - return prev; -}, { ...initialStats }); diff --git a/src/utils/calc/index.ts b/src/utils/calc/index.ts index c24bf2a1..f9d82211 100644 --- a/src/utils/calc/index.ts +++ b/src/utils/calc/index.ts @@ -2,7 +2,7 @@ export * from './calcCalcdexId'; export * from './calcCalcdexNonce'; export * from './calcPokemonFinalStats'; export * from './calcPokemonHp'; -export * from './calcPokemonStats'; +export * from './calcPokemonSpreadStats'; export * from './calcSmogonMatchup'; export * from './createSmogonField'; export * from './createSmogonMove'; From ce7a8d86266ab5cf3ca2194a9de47635d27e5e98 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Sat, 20 Aug 2022 23:20:38 -0700 Subject: [PATCH 074/143] refac(utils): removed exports on unused Calcdex nonce funcs --- src/utils/calc/calcCalcdexNonce.ts | 49 ++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/src/utils/calc/calcCalcdexNonce.ts b/src/utils/calc/calcCalcdexNonce.ts index 9bf2f27b..cf1b0735 100644 --- a/src/utils/calc/calcCalcdexNonce.ts +++ b/src/utils/calc/calcCalcdexNonce.ts @@ -3,8 +3,19 @@ import { env } from '@showdex/utils/core'; import type { CalcdexPokemon } from '@showdex/redux/store'; import { calcCalcdexId } from './calcCalcdexId'; -export const calcPokemonCalcdexNonce = ( - pokemon: Partial, +/** + * Calculates the nonce of a `Showdown.Pokemon` object, + * used to determine changes in the `battle` state. + * + * * As of v0.1.3, `calcdexNonce` of `CalcdexPokemon` is deprecated. + * - Since this is used in `calcBattleCalcdexNonce()`, which is still being used, + * this function is not deprecated. + * - However, this function is no longer exported. + * + * @since 0.1.0 + */ +const calcPokemonCalcdexNonce = ( + pokemon: DeepPartial, ): string => calcCalcdexId>>({ ident: pokemon?.ident, name: pokemon?.name, @@ -46,17 +57,28 @@ export const calcPokemonCalcdexNonce = ( return prev; }, >>{})), moveState: calcCalcdexId>(pokemon?.moveState), - boosts: calcCalcdexId>(pokemon?.boosts), - dirtyBoosts: calcCalcdexId>(pokemon?.dirtyBoosts), - baseStats: calcCalcdexId>(pokemon?.baseStats), - calculatedStats: calcCalcdexId>(pokemon?.calculatedStats), + boosts: calcCalcdexId(pokemon?.boosts), + dirtyBoosts: calcCalcdexId(pokemon?.dirtyBoosts), + baseStats: calcCalcdexId(pokemon?.baseStats), + spreadStats: calcCalcdexId(pokemon?.spreadStats), criticalHit: pokemon?.criticalHit?.toString(), preset: pokemon?.preset, presets: pokemon?.presets?.map((p) => p?.calcdexId || p?.name).join('|'), autoPreset: pokemon?.autoPreset?.toString(), }); -export const calcSideCalcdexNonce = ( +/** + * Calculates the nonce of a `Showdown.Side` object, + * used to determine changes in the `battle` state. + * + * * As of v0.1.3, `calcdexNonce` of `CalcdexPlayerSide` is deprecated. + * - Since this is used in `calcBattleCalcdexNonce()`, which is still being used, + * this function is not deprecated. + * - However, this function is no longer exported. + * + * @since 0.1.0 + */ +const calcSideCalcdexNonce = ( side: Partial, ): string => calcCalcdexId>>({ id: side?.id, @@ -69,6 +91,13 @@ export const calcSideCalcdexNonce = ( sideConditions: Object.keys(side?.sideConditions || {}).join('|'), }); +/** + * Calculates the nonce of the battle state. + * + * @todo Would probably be more performant to read from the `stepQueue`, + * but make sure doing so doesn't prevent any updates. + * @since 0.1.0 + */ export const calcBattleCalcdexNonce = ( battle: Partial, ): string => calcCalcdexId>>({ @@ -83,7 +112,7 @@ export const calcBattleCalcdexNonce = ( p3: calcSideCalcdexNonce(battle?.p3), p4: calcSideCalcdexNonce(battle?.p4), currentStep: battle?.currentStep?.toString(), - stepQueue: Array.isArray(battle?.stepQueue) && battle.stepQueue.length ? - uuidv5(battle.stepQueue.join('|'), env('uuid-namespace', NIL_UUID)) : - null, + stepQueue: Array.isArray(battle?.stepQueue) && battle.stepQueue.length + ? uuidv5(battle.stepQueue.join('|'), env('uuid-namespace', NIL_UUID)) + : null, }); From ba4b65c4730d5234037c0d1c15f6876b7bd16446 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Sat, 20 Aug 2022 23:20:58 -0700 Subject: [PATCH 075/143] docs(utils): added some notes on calcPokemonHp --- src/utils/calc/calcPokemonHp.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/utils/calc/calcPokemonHp.ts b/src/utils/calc/calcPokemonHp.ts index 7dd27c16..61ff3f12 100644 --- a/src/utils/calc/calcPokemonHp.ts +++ b/src/utils/calc/calcPokemonHp.ts @@ -1,5 +1,17 @@ import type { CalcdexPokemon } from '@showdex/redux/store'; +/** + * Name is a bit misleading because this returns the **percentage** of the Pokemon's + * remaining HP, not the actual numerical value. + * + * * You'll need to multiply this percentage with the actual max HP value to estimate + * the Pokemon's remaining HP value. + * - Actual max HP value can be derived from the `maxhp` of a `ServerPokemon` or + * calculating the HP stat value after EVs/IVs/nature are applied. + * + * @todo Rename this into what it actually does lol + * @since 0.1.0 + */ export const calcPokemonHp = ( pokemon: DeepPartial | DeepPartial, ): CalcdexPokemon['hp'] => (pokemon?.hp || 0) / (pokemon?.maxhp || 1); From 7e489d0cdb540ceac0c1bd1d652cfc9981e17aeb Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Sat, 20 Aug 2022 23:21:57 -0700 Subject: [PATCH 076/143] refac(utils): cleaned up speciesForme in createSmogonMove --- src/utils/calc/createSmogonMove.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/calc/createSmogonMove.ts b/src/utils/calc/createSmogonMove.ts index 903a9f35..2f12eabb 100644 --- a/src/utils/calc/createSmogonMove.ts +++ b/src/utils/calc/createSmogonMove.ts @@ -12,7 +12,7 @@ export const createSmogonMove = ( } const smogonMove = new SmogonMove(dex, moveName, { - species: pokemon?.rawSpeciesForme || pokemon?.speciesForme, + species: pokemon?.speciesForme, ability: pokemon?.dirtyAbility ?? pokemon?.ability, item: pokemon?.dirtyItem ?? pokemon?.item, useZ: dex.num === 7 && pokemon?.useUltimateMoves, From d06ccd2f47d9a5c1f9106dbb18566612f077f009 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Sat, 20 Aug 2022 23:24:11 -0700 Subject: [PATCH 077/143] refac(utils): cleaned up createSmogonPokemon --- src/utils/calc/createSmogonPokemon.ts | 141 +++++++++++++------------- 1 file changed, 68 insertions(+), 73 deletions(-) diff --git a/src/utils/calc/createSmogonPokemon.ts b/src/utils/calc/createSmogonPokemon.ts index 4f77465a..257f62e2 100644 --- a/src/utils/calc/createSmogonPokemon.ts +++ b/src/utils/calc/createSmogonPokemon.ts @@ -3,63 +3,56 @@ import { PokemonToggleAbilities } from '@showdex/consts'; import { detectPokemonIdent, detectSpeciesForme } from '@showdex/utils/battle'; import { logger } from '@showdex/utils/debug'; import type { Generation, MoveName } from '@pkmn/data'; -import type { State as SmogonState } from '@smogon/calc'; import type { CalcdexPokemon } from '@showdex/redux/store'; import { calcPokemonHp } from './calcPokemonHp'; -// import { calcPokemonStats } from './calcPokemonStats'; -const l = logger('@showdex/pages/Calcdex/createSmogonPokemon'); +const l = logger('@showdex/utils/calc/createSmogonPokemon'); export const createSmogonPokemon = ( dex: Generation, pokemon: CalcdexPokemon, moveName?: MoveName, ): SmogonPokemon => { + // don't bother logging in this and the `ident` check below cause the Calcdex components + // may get partial data (or even nothing) in the beginning, so these logs would get pretty spammy if (typeof dex?.num !== 'number' || dex.num < 1) { - // l.warn( - // 'received an invalid gen value', - // '\n', 'gen', gen, - // '\n', 'pokemon', pokemon, - // ); - return null; } const ident = detectPokemonIdent(pokemon); if (!ident) { - // l.debug( - // 'createSmogonPokemon() <- detectPokemonIdent()', - // '\n', 'failed to detect Pokemon\'s ident', - // '\n', 'ident', ident, - // '\n', 'gen', gen, - // '\n', 'pokemon', pokemon, - // ); - return null; } + // optional chaining here since `item` can be cleared by the user (dirtyItem) in PokeInfo + // (note: when cleared, `dirtyItem` will be set to null, which will default to `item`) + const item = pokemon?.dirtyItem ?? pokemon?.item; + const speciesForme = SmogonPokemon.getForme( dex, detectSpeciesForme(pokemon), - pokemon?.dirtyItem || pokemon?.item, + item, moveName, ); // shouldn't happen, but just in case, ja feel if (!speciesForme) { - l.warn( - 'createSmogonPokemon() <- detectSpeciesForme()', - '\n', 'failed to detect speciesForme from Pokemon with ident', ident, - '\n', 'speciesForme', speciesForme, - '\n', 'gen', dex.num, - '\n', 'pokemon', pokemon, - ); + if (__DEV__) { + l.warn( + 'Failed to detect speciesForme from Pokemon with ident', ident, + '\n', 'speciesForme', speciesForme, + '\n', 'gen', dex.num, + '\n', 'pokemon', pokemon, + '\n', '(You will only see this warning on development.)', + ); + } return null; } - let ability = pokemon?.dirtyAbility ?? pokemon?.ability; + // not using optional chaining here since ability cannot be cleared in PokeInfo + const ability = pokemon.dirtyAbility || pokemon.ability; // note: Multiscale is in the PokemonToggleAbilities list, but isn't technically toggleable, per se. // we only allow it to be toggled on/off since it works like a Focus Sash (i.e., depends on the Pokemon's HP). @@ -67,69 +60,71 @@ export const createSmogonPokemon = ( const hasMultiscale = ability?.toLowerCase?.() === 'multiscale'; const toggleAbility = !hasMultiscale && PokemonToggleAbilities.includes(ability); - if (hasMultiscale && !pokemon?.abilityToggled) { - ability = null; - } - const options: ConstructorParameters[2] = { - ...( pokemon), + // note: curHP and originalCurHP in the SmogonPokemon's constructor both set the originalCurHP + // of the class instance with curHP's value taking precedence over originalCurHP's value + // (in other words, seems safe to specify either one, but if none, defaults to rawStats.hp) + // --- + // also note: seems that maxhp is internally calculated in the instance's rawStats.hp, + // so we can't specify it here + curHP: pokemon.serverSourced ? pokemon.hp : (() => { // js wizardry + // note that spreadStats may not be available yet, hence the fallback object + const hpPercentage = calcPokemonHp(pokemon); + const { hp: hpStat } = pokemon.spreadStats || { hp: pokemon?.maxhp || 1 }; + + // cheeky way to allow the user to "turn off" Multiscale w/o editing the HP value + // (if true, we'll tell the Smogon calc that it's at 99% HP) + // (also, the ability toggle button in PokeInfo will disable if the HP isn't 100%) + const shouldMultiscale = hasMultiscale && hpPercentage === 1 && !pokemon?.abilityToggled; + + // if the Pokemon fainted, assume it has full HP as to not break the damage calc + return (shouldMultiscale ? 0.99 : hpPercentage || 1) * hpStat; + })(), + + level: pokemon.level, + gender: pokemon.gender, + + // appears that the SmogonPokemon will automatically double both the HP and max HP if this is true, + // which I'd imagine affects the damage calculations in the matchup + // (useUltimateMoves is a gen-agnostic property that's user-toggleable and syncs w/ the battle state btw) + isDynamaxed: pokemon.useUltimateMoves, ability, - abilityOn: toggleAbility ? pokemon?.abilityToggled : undefined, - - item: pokemon?.dirtyItem ?? pokemon?.item, + abilityOn: toggleAbility ? pokemon.abilityToggled : undefined, + item: pokemon.dirtyItem || pokemon.item, + nature: pokemon.nature, + moves: pokemon.moves, ivs: { - hp: pokemon?.ivs?.hp ?? 31, - atk: pokemon?.ivs?.atk ?? 31, - def: pokemon?.ivs?.def ?? 31, - spa: pokemon?.ivs?.spa ?? 31, - spd: pokemon?.ivs?.spd ?? 31, - spe: pokemon?.ivs?.spe ?? 31, + hp: pokemon.ivs?.hp ?? 31, + atk: pokemon.ivs?.atk ?? 31, + def: pokemon.ivs?.def ?? 31, + spa: pokemon.ivs?.spa ?? 31, + spd: pokemon.ivs?.spd ?? 31, + spe: pokemon.ivs?.spe ?? 31, }, evs: { - hp: pokemon?.evs?.hp ?? 0, - atk: pokemon?.evs?.atk ?? 0, - def: pokemon?.evs?.def ?? 0, - spa: pokemon?.evs?.spa ?? 0, - spd: pokemon?.evs?.spd ?? 0, - spe: pokemon?.evs?.spe ?? 0, + hp: pokemon.evs?.hp ?? 0, + atk: pokemon.evs?.atk ?? 0, + def: pokemon.evs?.def ?? 0, + spa: pokemon.evs?.spa ?? 0, + spd: pokemon.evs?.spd ?? 0, + spe: pokemon.evs?.spe ?? 0, }, boosts: { - atk: pokemon?.dirtyBoosts?.atk ?? pokemon?.boosts?.atk ?? 0, - def: pokemon?.dirtyBoosts?.def ?? pokemon?.boosts?.def ?? 0, - spa: pokemon?.dirtyBoosts?.spa ?? pokemon?.boosts?.spa ?? 0, - spd: pokemon?.dirtyBoosts?.spd ?? pokemon?.boosts?.spd ?? 0, - spe: pokemon?.dirtyBoosts?.spe ?? pokemon?.boosts?.spe ?? 0, + atk: pokemon.dirtyBoosts?.atk ?? pokemon.boosts?.atk ?? 0, + def: pokemon.dirtyBoosts?.def ?? pokemon.boosts?.def ?? 0, + spa: pokemon.dirtyBoosts?.spa ?? pokemon.boosts?.spa ?? 0, + spd: pokemon.dirtyBoosts?.spd ?? pokemon.boosts?.spd ?? 0, + spe: pokemon.dirtyBoosts?.spe ?? pokemon.boosts?.spe ?? 0, }, }; - // calculate the Pokemon's current HP - // const calculatedStats = calcPokemonStats(dex, pokemon); - - // if (typeof calculatedStats?.hp === 'number') { - // const currentHp = calcPokemonHp(pokemon); - // const { hp: hpStat } = calculatedStats; - // - // // if the Pokemon fainted, assume it has full HP as to not break the damage calc - // options.curHP = (currentHp || 1) * hpStat; - // } - - const smogonPokemon = new SmogonPokemon( + return new SmogonPokemon( dex, speciesForme, options, ); - - if (typeof smogonPokemon?.rawStats?.hp === 'number') { - const currentHp = calcPokemonHp(pokemon); - const { hp: hpStat } = smogonPokemon.rawStats; - - // if the Pokemon fainted, assume it has full HP as to not break the damage calc - smogonPokemon.originalCurHP = (currentHp || 1) * hpStat; - } - - return smogonPokemon; }; From 7077b1d3d342b4e9a8614db1cc382c606747514b Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Sat, 20 Aug 2022 23:25:44 -0700 Subject: [PATCH 078/143] feat(utils): completed finalStat calculation --- src/utils/calc/calcPokemonFinalStats.ts | 469 ++++++++++++++++++++---- 1 file changed, 401 insertions(+), 68 deletions(-) diff --git a/src/utils/calc/calcPokemonFinalStats.ts b/src/utils/calc/calcPokemonFinalStats.ts index 1a06f99f..7fb24061 100644 --- a/src/utils/calc/calcPokemonFinalStats.ts +++ b/src/utils/calc/calcPokemonFinalStats.ts @@ -1,50 +1,55 @@ -import { getFinalSpeed, getModifiedStat } from '@smogon/calc/dist/mechanics/util'; -import { PokemonStatNames } from '@showdex/consts'; +// import { getFinalSpeed, getModifiedStat } from '@smogon/calc/dist/mechanics/util'; +import { PokemonInitialStats, PokemonSpeedReductionItems, PokemonStatNames } from '@showdex/consts'; +import { formatId as id } from '@showdex/utils/app'; // import { detectSpeciesForme } from '@showdex/utils/battle'; -// import { logger } from '@showdex/utils/debug'; -import type { Generation } from '@pkmn/data'; -import type { CalcdexBattleField, CalcdexPokemon } from '@showdex/redux/store'; -// import { calcPokemonHp } from './calcPokemonHp'; -import { createSmogonField } from './createSmogonField'; -import { createSmogonPokemon } from './createSmogonPokemon'; - -// const l = logger('@showdex/pages/Calcdex/calcPokemonStats'); - -const initialStats: Showdown.StatsTable = { - hp: 0, - atk: 0, - def: 0, - spa: 0, - spd: 0, - spe: 0, -}; +import { env } from '@showdex/utils/core'; +import { logger } from '@showdex/utils/debug'; +import type { Generation, GenerationNum } from '@pkmn/data'; +import type { CalcdexBattleField, CalcdexPlayerKey, CalcdexPokemon } from '@showdex/redux/store'; +import { calcPokemonHp } from './calcPokemonHp'; +// import { createSmogonField } from './createSmogonField'; +// import { createSmogonPokemon } from './createSmogonPokemon'; + +const l = logger('@showdex/utils/calc/calcPokemonFinalStats'); /** - * @todo Flower Gift abililty (factor in dmax). - * @todo Fur Coat ability. - * @todo Choice Band/Scarf/Specs (factor in dmax). - * @todo Gorilla Tactics ability (factor in dmax). - * @todo Grass Pelt ability. - * @todo Huge Power ability. - * @todo Hustle ability. - * @todo Marvel Scale ability. + * Reimplementation of `calculateModifiedStats()` in the Showdown client's `BattleTooltip`. + * + * * Makes use of our custom `CalcdexPokemon` and `CalcdexBattleField` properties wherever possible + * to better integrate with the Calcdex's state (which many properties are user-mutable). + * * Though the aforementioned function exists in the client, it reads directly from the battle state, + * preventing us from non-destructively incorporating the Calcdex's state. + * - In other words, would be a pain in the ass to incorporate the user's inputs into the function, + * which requires writing directly into the battle state, hoping that we didn't fuck something up. + * - Let's not forget that we gotta get our `CalcdexBattleField` in there too! + * * Hence why I chose death. + * + * @see https://github.com/smogon/pokemon-showdown-client/blob/master/src/battle-tooltips.ts#L959-L1213 * @since 0.1.3 */ export const calcPokemonFinalStats = ( dex: Generation, pokemon: DeepPartial, + opponentPokemon: DeepPartial, field: CalcdexBattleField, - side: 'attacker' | 'defender', + playerKey: CalcdexPlayerKey, ): Showdown.StatsTable => { - // if (typeof dex?.stats?.calc !== 'function' || typeof dex?.species?.get !== 'function') { - // l.warn( - // 'cannot calculate stats since dex.stats.calc() and/or dex.species.get() are not available', - // '\n', 'dex', dex, - // '\n', 'pokemon', pokemon, - // ); - // - // return initialStats; - // } + if (typeof dex?.stats?.calc !== 'function' || typeof dex?.species?.get !== 'function') { + if (__DEV__) { + l.warn( + 'Cannot calculate stats since dex.stats.calc() and/or dex.species.get() are not available.', + '\n', 'dex', dex, + '\n', 'pokemon', pokemon, + '\n', 'field', field, + '\n', 'playerKey', playerKey, + '\n', '(You will only see this warning on development.)', + ); + } + + return { ...PokemonInitialStats }; + } + + const gen = dex.num || env.int('calcdex-default-gen'); // const speciesForme = detectSpeciesForme(pokemon); @@ -61,6 +66,11 @@ export const calcPokemonFinalStats = ( // const species = dex.species.get(speciesForme); + const hpPercentage = calcPokemonHp(pokemon); + + const ability = id(pokemon.dirtyAbility || pokemon.ability); + const opponentAbility = id(opponentPokemon.dirtyAbility || opponentPokemon.ability); + // const nature = pokemon?.nature && typeof dex.natures?.get === 'function' ? // dex.natures.get(pokemon.nature) : // undefined; @@ -71,8 +81,8 @@ export const calcPokemonFinalStats = ( // const hasQuickFeet = pokemon?.ability?.toLowerCase?.() === 'quick feet'; // const hasDefeatist = pokemon?.ability?.toLowerCase() === 'defeatist'; - const smogonPokemon = createSmogonPokemon(dex, pokemon); - const smogonField = createSmogonField(field); + // const smogonPokemon = createSmogonPokemon(dex, pokemon); + // const smogonField = createSmogonField(field); // rebuild the Pokemon's base stats to make sure all values are available // const baseStats: CalcdexPokemon['baseStats'] = { @@ -94,7 +104,7 @@ export const calcPokemonFinalStats = ( // }; // do the same thing for the Pokemon's IVs and EVs - // const ivs: CalcdexPokemon['ivs'] = { + // const ivs: Showdown.StatsTable = { // hp: pokemon?.ivs?.hp ?? 31, // atk: pokemon?.ivs?.atk ?? 31, // def: pokemon?.ivs?.def ?? 31, @@ -103,7 +113,7 @@ export const calcPokemonFinalStats = ( // spe: pokemon?.ivs?.spe ?? 31, // }; - // const evs: CalcdexPokemon['evs'] = { + // const evs: Showdown.StatsTable = { // hp: pokemon?.evs?.hp ?? 0, // atk: pokemon?.evs?.atk ?? 0, // def: pokemon?.evs?.def ?? 0, @@ -112,34 +122,38 @@ export const calcPokemonFinalStats = ( // spe: pokemon?.evs?.spe ?? 0, // }; - // const boosts: CalcdexPokemon['boosts'] = { - // atk: (pokemon?.dirtyBoosts?.atk ?? pokemon?.boosts?.atk) || 0, - // def: (pokemon?.dirtyBoosts?.def ?? pokemon?.boosts?.def) || 0, - // spa: (pokemon?.dirtyBoosts?.spa ?? pokemon?.boosts?.spa) || 0, - // spd: (pokemon?.dirtyBoosts?.spd ?? pokemon?.boosts?.spd) || 0, - // spe: (pokemon?.dirtyBoosts?.spe ?? pokemon?.boosts?.spe) || 0, - // }; + const boostTable = gen > 2 + ? [1, 1.5, 2, 2.5, 3, 3.5, 4] + : [1, 100 / 66, 2, 2.5, 100 / 33, 100 / 28, 4]; - const calculatedStats: CalcdexPokemon['calculatedStats'] = PokemonStatNames.reduce((prev, stat) => { - if (stat === 'spe') { - const fieldSide = side === 'attacker' ? smogonField.attackerSide : smogonField.defenderSide; + const boosts: Showdown.StatsTable = { + atk: (pokemon?.dirtyBoosts?.atk ?? pokemon?.boosts?.atk) || 0, + def: (pokemon?.dirtyBoosts?.def ?? pokemon?.boosts?.def) || 0, + spa: (pokemon?.dirtyBoosts?.spa ?? pokemon?.boosts?.spa) || 0, + spd: (pokemon?.dirtyBoosts?.spd ?? pokemon?.boosts?.spd) || 0, + spe: (pokemon?.dirtyBoosts?.spe ?? pokemon?.boosts?.spe) || 0, + }; - prev.spe = getFinalSpeed( - dex, - smogonPokemon, - smogonField, - fieldSide, - ); - } else { - prev[stat] = getModifiedStat( - smogonPokemon.rawStats[stat], - smogonPokemon.boosts[stat], - dex, - ); - } - - return prev; - }, { ...initialStats }); + // const calculatedStats: CalcdexPokemon['calculatedStats'] = PokemonStatNames.reduce((prev, stat) => { + // if (stat === 'spe') { + // const fieldSide = side === 'attacker' ? smogonField.attackerSide : smogonField.defenderSide; + // + // prev.spe = getFinalSpeed( + // dex, + // smogonPokemon, + // smogonField, + // fieldSide, + // ); + // } else { + // prev[stat] = getModifiedStat( + // smogonPokemon.rawStats[stat], + // smogonPokemon.boosts[stat], + // dex, + // ); + // } + // + // return prev; + // }, { ...initialStats }); // const calculatedStats: CalcdexPokemon['calculatedStats'] = PokemonStatNames.reduce((prev, stat) => { // prev[stat] = dex.stats.calc( @@ -235,5 +249,324 @@ export const calcPokemonFinalStats = ( // '\n', 'speciesForme', speciesForme, // ); - return calculatedStats; + const currentStats: Showdown.StatsTable = { + ...PokemonInitialStats, + ...pokemon.baseStats, + ...pokemon.spreadStats, + ...pokemon.serverStats, + }; + + const hasTransform = 'transform' in pokemon.volatiles; + const hasFormeChange = 'formechange' in pokemon.volatiles; + + const speciesForme = hasTransform && hasFormeChange + ? pokemon.volatiles.formechange[1] + : pokemon.speciesForme; + + const species = dex.species.get(speciesForme); + const baseForme = id(species?.baseSpecies); + + const hasPowerTrick = 'powertrick' in pokemon.volatiles; // this is a move btw, not an ability! + const hasEmbargo = 'embargo' in pokemon.volatiles; // this is also a move + const hasGuts = ability === 'guts'; + const hasQuickFeet = ability === 'quickfeet'; + + const item = id(pokemon.dirtyItem ?? pokemon.item); + + const ignoreItem = hasEmbargo + || field.isMagicRoom + || (ability === 'klutz' && !PokemonSpeedReductionItems.map((i) => id(i)).includes(item)); + + const finalStats: Showdown.StatsTable = PokemonStatNames.reduce((stats, stat) => { + // apply effects to non-HP stats + if (stat !== 'hp') { + // swap ATK and DEF if "Power Trick" was used + if (hasPowerTrick) { + stats[stat] = currentStats[stat === 'atk' ? 'def' : 'atk'] || 0; + } + + // apply stat boosts if not 0 (cause it'd do nothing) + const stage = boosts[stat]; + + if (stage) { + const boostValue = boostTable[Math.abs(stage)]; + const boostMultiplier = stage > 0 ? boostValue : 1 / boostValue; + + stats[stat] = Math.floor(stats[stat] * boostMultiplier); + } + + // apply status condition effects + if (pokemon.status) { + if (gen > 2 && ((stat === 'atk' && hasGuts) || (stat === 'spe' && hasQuickFeet))) { + // 50% ATK boost w/ non-volatile status condition due to "Guts" (gen 3+) + // 50% SPE boost w/ non-volatile status condition due to "Quick Feet" (gen 4+) + stats[stat] = Math.floor(stats[stat] * 1.5); + } + + switch (pokemon.status) { + case 'brn': { + if (stat === 'atk') { + // 50% ATK reduction (all gens... probably) + stats[stat] = Math.floor(stats[stat] * 0.5); + } + + break; + } + + case 'par': { + if (stat === 'spe' && (gen < 4 || !hasQuickFeet)) { + // 75% SPE reduction if gen < 7 (i.e., gens 1-6), otherwise 50% SPE reduction + // (reduction is negated if ability is "Quick Feet", which is only available gen 4+) + stats[stat] = Math.floor(stats[stat] * (gen < 7 ? 0.25 : 0.5)); + } + + break; + } + + default: { + break; + } + } + } + } + + if (gen <= 1) { + stats[stat] = Math.min(stats[stat], 999); + } + + return stats; + }, { ...currentStats }); + + // gen 1 doesn't support items + if (gen <= 1) { + return finalStats; + } + + // apply gen 2-compatible item effects + // (at this point, we should at least be gen 2) + const speedMods: number[] = []; + + if (baseForme === 'pikachu' && !ignoreItem && item === 'lightball') { + if (gen > 4) { + // 100% ATK boost if "Light Ball" is held by a Pikachu (gen 5+) + finalStats.atk = Math.floor(finalStats.atk * 2); + } + + // 100% SPA boost if "Light Ball" is held by a Pikachu + finalStats.spa = Math.floor(finalStats.spa * 2); + } + + if (['marowak', 'cubone'].includes(baseForme) && !ignoreItem && item === 'thickclub') { + // 100% ATK boost if "Thick Club" is held by a Marowak/Cubone + finalStats.atk = Math.floor(finalStats.atk * 2); + } + + if (baseForme === 'ditto' && !hasTransform && !ignoreItem) { + if (item === 'quickpowder') { + speedMods.push(2); + } else if (item === 'metalpowder') { + if (gen === 2) { + // 50% DEF/SPD boost if "Metal Powder" is held by a Ditto (gen 2) + finalStats.def = Math.floor(finalStats.def * 1.5); + finalStats.spd = Math.floor(finalStats.spd * 1.5); + } else { + // 100% DEF boost if "Metal Powder" is held by a Ditto (gen 3+) + finalStats.def = Math.floor(finalStats.def * 2); + } + } + } + + // finished gen 2 abilities and items + if (gen <= 2) { + return finalStats; + } + + const hasDynamax = 'dynamax' in pokemon.volatiles; + + // apply more item effects + // (at this point, we should at least be gen 3) + if (!ignoreItem) { + if (item === 'choiceband' && !hasDynamax) { + // 50% ATK boost if "Choice Band" is held + finalStats.atk = Math.floor(finalStats.atk * 1.5); + } + + if (item === 'choicespecs' && !hasDynamax) { + // 50% SPA boost if "Choice Specs" is held + finalStats.spa = Math.floor(finalStats.spa * 1.5); + } + + if (item === 'choicescarf' && !hasDynamax) { + speedMods.push(1.5); + } + + if (item === 'assaultvest') { + // 50% SPA boost if "Assault Vest" is held + finalStats.spd = Math.floor(finalStats.spd * 1.5); + } + + if (item === 'furcoat') { + // 100% DEF boost if "Fur Coat" is held + finalStats.def = Math.floor(finalStats.def * 2); + } + + if (baseForme === 'clamperl') { + if (item === 'deepseatooth') { + // 100% SPA boost if "Deep Sea Tooth" is held by a Clamperl + finalStats.spa = Math.floor(finalStats.spa * 2); + } else if (item === 'deepseascale') { + // 100% SPD boost if "Deep Sea Scale" is held by a Clamperl + finalStats.spd = Math.floor(finalStats.spd * 2); + } + } + + if (item === 'souldew' && gen < 7 && ['latios', 'latias'].includes(baseForme)) { + // 50% SPA/SPD boost if "Soul Dew" is held by a Latios/Latias (gens 3-6) + finalStats.spa = Math.floor(finalStats.spa * 1.5); + finalStats.spd = Math.floor(finalStats.spd * 1.5); + } + + const speedReductionItems = [ + 'ironball', + ...PokemonSpeedReductionItems.map((i) => id(i)), + ]; + + if (speedReductionItems.includes(item)) { + speedMods.push(0.5); + } + } + + if (['purepower', 'hugepower'].includes(ability)) { + // 100% ATK boost if ability is "Pure Power" or "Huge Power" + finalStats.atk = Math.floor(finalStats.atk * 2); + } + + if (ability === 'hustle' || (ability === 'gorillatactics' && !hasDynamax)) { + // 50% ATK boost if ability is "Hustle" or "Gorilla Tactics" (and not dynamaxed, for the latter only) + finalStats.atk = Math.floor(finalStats.atk * 1.5); + } + + // apply weather effects + const weather = id(field.weather); + + const ignoreWeather = [ + ability, + opponentAbility, + ].filter(Boolean).some((a) => ['airlock', 'cloudnine'].includes(a)); + + if (weather && !ignoreWeather) { + if (weather === 'sandstorm') { + if (pokemon.types.includes('Rock')) { + // 50% SPE boost if Rock type w/ darude sandstorm + finalStats.spe = Math.floor(finalStats.spe * 1.5); + } + + if (ability === 'sandrush') { + speedMods.push(2); + } + } + + if (ability === 'slushrush' && weather === 'hail') { + speedMods.push(2); + } + + if (ignoreItem || item !== 'utilityumbrella') { + if (['sunnyday', 'desolateland'].includes(weather)) { + if (ability === 'solarpower') { + // 50% SPA boost if ability is "Solar Power", sunny/desolate, and Pokemon is NOT holding "Utility Umbrella" + finalStats.spa = Math.floor(finalStats.spa * 1.5); + } else if (ability === 'chlorophyll') { + speedMods.push(2); + } + + /** + * @todo Implement ally Pokemon, notably Cherrim's "Flower Gift" + */ + } + } + + if (['raindance', 'primordialsea'].includes(weather) && ability === 'swiftswim') { + speedMods.push(2); + } + } + + // yoo when tf did they make me into an ability lmaooo + if (ability === 'defeatist' && hpPercentage <= 0.5) { + // 50% ATK/SPA reduction if ability is "Defeatist" and HP is 50% or less + finalStats.atk = Math.floor(finalStats.atk * 0.5); + finalStats.spa = Math.floor(finalStats.spa * 0.5); + } + + // apply toggleable abilities + if (pokemon.abilityToggled) { + if ('slowstart' in pokemon.volatiles) { + // 50% ATK/SPA reduction if ability is "Slow Start" + finalStats.atk = Math.floor(finalStats.atk * 0.5); + finalStats.spa = Math.floor(finalStats.spa * 0.5); + } + + if (ability === 'unburden' && !item && 'itemremoved' in pokemon.volatiles) { + speedMods.push(2); + } + + /** + * @todo implement ally Pokemon for "Minus" and "Plus" toggleable abilities + */ + } + + // apply additional status effects + if (pokemon.status) { + if (ability === 'marvelscale') { + // 50% DEF boost if ability is "Marvel Scale" and Pokemon is statused + finalStats.def = Math.floor(finalStats.def * 1.5); + } + } + + // apply NFE (not fully evolved) effects + const nfe = species?.evos?.some((evo) => { + const evoSpecies = dex.species.get(evo); + + return !evoSpecies?.isNonstandard + || evoSpecies?.isNonstandard === species.isNonstandard; + }); + + if (nfe) { + if (!ignoreItem && item === 'eviolite') { + // 50% DEF/SPD boost if "Eviolite" is held by an NFE Pokemon + finalStats.def = Math.floor(finalStats.def * 1.5); + finalStats.spd = Math.floor(finalStats.spd * 1.5); + } + } + + // apply terrain effects + const terrain = id(field.terrain); + + if (ability === 'grasspelt' && terrain === 'grassy') { + // 50% DEF boost if ability is "Grass Pelt" and terrain is of the grassy nature + finalStats.def = Math.floor(finalStats.def * 1.5); + } else if (ability === 'surgesurfer' && terrain === 'electric') { + speedMods.push(2); + } + + // apply player side conditions + const fieldSideKey = playerKey === 'p1' ? 'attackerSide' : 'defenderSide'; + const playerSide = field[fieldSideKey]; + + if (playerSide?.isTailwind) { + speedMods.push(2); + } + + if (playerSide?.isGrassPledge) { + speedMods.push(0.25); + } + + // calculate the product of all the speedMods + const speedMod = speedMods.reduce((acc, mod) => acc * mod, 1); + + // apply the speedMod, rounding down on 0.5 and below + // (unlike Math.round(), which rounds up on 0.5 and above) + finalStats.spe *= speedMod; + finalStats.spe = finalStats.spe % 1 > 0.5 ? Math.ceil(finalStats.spe) : Math.floor(finalStats.spe); + + return finalStats; }; From fbcc232bfbebb00d980acf659ddf191a43b87a77 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Sat, 20 Aug 2022 23:26:08 -0700 Subject: [PATCH 079/143] refac(calcdex): cleaned up speciesForme in PlayerCalc --- src/pages/Calcdex/PlayerCalc.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/Calcdex/PlayerCalc.tsx b/src/pages/Calcdex/PlayerCalc.tsx index f69a5bc4..6360f527 100644 --- a/src/pages/Calcdex/PlayerCalc.tsx +++ b/src/pages/Calcdex/PlayerCalc.tsx @@ -125,7 +125,7 @@ export const PlayerCalc = ({ const mon = pokemon?.[i]; const pokemonKey = mon?.calcdexId || mon?.ident || defaultName || '???'; - const friendlyPokemonName = mon?.rawSpeciesForme || mon?.speciesForme || mon?.name || pokemonKey; + const friendlyPokemonName = mon?.speciesForme || mon?.name || pokemonKey; return ( onPokemonChange?.(playerKey, p)} /> From d59cdbfb97988df267f4d32442ed842e79cedde5 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Sun, 21 Aug 2022 01:49:23 -0700 Subject: [PATCH 080/143] refac(calcdex): removed some debug logs in usePresets --- src/pages/Calcdex/usePresets.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/pages/Calcdex/usePresets.ts b/src/pages/Calcdex/usePresets.ts index 552f9c43..cea9ce32 100644 --- a/src/pages/Calcdex/usePresets.ts +++ b/src/pages/Calcdex/usePresets.ts @@ -189,10 +189,10 @@ export const usePresets = ({ return []; } - l.debug( - 'Attempting to find presets for', speciesForme, - '\n', 'sort', sort || false, - ); + // l.debug( + // 'Attempting to find presets for', speciesForme, + // '\n', 'sort', sort || false, + // ); // e.g., evals to true w/ speciesForme 'Urshifu-Rapid-Strike-Gmax' or 'Charizard-Mega-X' const hasUltForme = UltFormeRegex.test(speciesForme); @@ -204,10 +204,10 @@ export const usePresets = ({ const ultPresets = presets.filter((p) => p.format.includes(genlessFormat) && p.speciesForme === speciesForme); if (ultPresets.length) { - l.debug( - 'Found ultPresets for', speciesForme, - // '\n', 'ultPresets', ultPresets, - ); + // l.debug( + // 'Found ultPresets for', speciesForme, + // // '\n', 'ultPresets', ultPresets, + // ); return sort ? sortPresets(ultPresets, genlessFormat) : ultPresets; } @@ -233,10 +233,10 @@ export const usePresets = ({ }); if (nonWildPresets.length) { - l.debug( - 'Found nonWildPresets for', speciesForme, - '\n', 'nonWildPresets', nonWildPresets, - ); + // l.debug( + // 'Found nonWildPresets for', speciesForme, + // '\n', 'nonWildPresets', nonWildPresets, + // ); return sort ? sortPresets(nonWildPresets, genlessFormat) : nonWildPresets; } From 088757dd9a951e05b9327dba3209e889f6bac468 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Sun, 21 Aug 2022 01:52:23 -0700 Subject: [PATCH 081/143] feat(calcdex): added props required for finalStats in PokeStats --- src/pages/Calcdex/PokeStats.tsx | 48 ++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/src/pages/Calcdex/PokeStats.tsx b/src/pages/Calcdex/PokeStats.tsx index f55dbbce..00c46a44 100644 --- a/src/pages/Calcdex/PokeStats.tsx +++ b/src/pages/Calcdex/PokeStats.tsx @@ -11,17 +11,19 @@ import { } from '@showdex/consts'; import { detectStatBoostDelta, formatStatBoost } from '@showdex/utils/battle'; import { calcPokemonFinalStats } from '@showdex/utils/calc'; +import { env } from '@showdex/utils/core'; import type { Generation } from '@pkmn/data'; -import type { CalcdexBattleField, CalcdexPokemon } from '@showdex/redux/store'; +import type { CalcdexBattleField, CalcdexPlayerKey, CalcdexPokemon } from '@showdex/redux/store'; import styles from './PokeStats.module.scss'; export interface PokeStatsProps { className?: string; style?: React.CSSProperties; dex: Generation; - pokemon: CalcdexPokemon; + playerPokemon: CalcdexPokemon; + opponentPokemon: CalcdexPokemon; field?: CalcdexBattleField; - side?: 'attacker' | 'defender'; + playerKey?: CalcdexPlayerKey, onPokemonChange?: (pokemon: DeepPartial) => void; } @@ -29,9 +31,10 @@ export const PokeStats = ({ className, style, dex, - pokemon, + playerPokemon: pokemon, + opponentPokemon, field, - side, + playerKey, onPokemonChange, }: PokeStatsProps): JSX.Element => { const colorScheme = useColorScheme(); @@ -39,10 +42,22 @@ export const PokeStats = ({ const pokemonKey = pokemon?.calcdexId || pokemon?.name || '???'; const friendlyPokemonName = pokemon?.speciesForme || pokemon?.name || pokemonKey; - const finalStats = React.useMemo( - () => (pokemon?.calcdexId ? calcPokemonFinalStats(dex, pokemon, field, side) : null), - [dex, pokemon, field, side], - ); + const totalEvs = Object.values(pokemon?.evs || {}).reduce((sum, ev) => sum + (ev || 0), 0); + const evsLegal = totalEvs <= env.int('calcdex-pokemon-max-legal-evs'); + + const finalStats = React.useMemo(() => (pokemon?.calcdexId ? calcPokemonFinalStats( + dex, + pokemon, + opponentPokemon, + field, + playerKey, + ) : null), [ + dex, + opponentPokemon, + playerKey, + pokemon, + field, + ]); return ( + EV S @@ -135,6 +158,7 @@ export const PokeStats = ({ className={styles.valueField} label={`${stat.toUpperCase()} EV for Pokemon ${friendlyPokemonName}`} hint={ev.toString() || '252'} + fallbackValue={0} min={0} max={252} step={4} @@ -157,8 +181,8 @@ export const PokeStats = ({ {PokemonStatNames.map((stat) => { - const calculatedStat = finalStats?.[stat] || 0; - const formattedStat = formatStatBoost(calculatedStat) || '???'; + const finalStat = finalStats?.[stat] || 0; + const formattedStat = formatStatBoost(finalStat) || '???'; const boostDelta = detectStatBoostDelta(pokemon, finalStats, stat); return ( From 85b7202d028775ef459dc10320c8c4d1ad87723b Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Sun, 21 Aug 2022 01:54:00 -0700 Subject: [PATCH 082/143] feat(calcdex): passed new PokeStats props in PokeCalc --- src/pages/Calcdex/PokeCalc.tsx | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/pages/Calcdex/PokeCalc.tsx b/src/pages/Calcdex/PokeCalc.tsx index de930197..dfdbca2a 100644 --- a/src/pages/Calcdex/PokeCalc.tsx +++ b/src/pages/Calcdex/PokeCalc.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import cx from 'classnames'; +import { detectToggledAbility, toggleableAbility } from '@showdex/utils/battle'; // import { logger } from '@showdex/utils/debug'; import type { Generation } from '@pkmn/data'; import type { GenerationNum } from '@pkmn/types'; @@ -20,7 +21,6 @@ interface PokeCalcProps { playerPokemon: CalcdexPokemon; opponentPokemon: CalcdexPokemon; field?: CalcdexBattleField; - side?: 'attacker' | 'defender'; onPokemonChange?: (pokemon: DeepPartial) => void; } @@ -36,7 +36,6 @@ export const PokeCalc = ({ playerPokemon, opponentPokemon, field, - side, onPokemonChange, }: PokeCalcProps): JSX.Element => { const calculateMatchup = useSmogonMatchup( @@ -54,10 +53,6 @@ export const PokeCalc = ({ ...mutation, calcdexId: playerPokemon?.calcdexId, - // ident: playerPokemon?.ident, - // boosts: playerPokemon?.boosts, - - // nature: mutation?.nature ?? playerPokemon?.nature, ivs: { ...playerPokemon?.ivs, @@ -75,6 +70,20 @@ export const PokeCalc = ({ }, }; + // re-check for toggleable abilities in the mutation + if ('ability' in mutation || 'dirtyAbility' in mutation) { + const tempPokemon = { + ...playerPokemon, + ...payload, + }; + + payload.abilityToggleable = toggleableAbility(tempPokemon); + + if (payload.abilityToggleable) { + payload.abilityToggled = detectToggledAbility(tempPokemon); + } + } + // clear any dirtyBoosts that match the current boosts Object.entries(playerPokemon.boosts).forEach(([ stat, @@ -124,9 +133,11 @@ export const PokeCalc = ({ From 675456582f39f90f78f32a8f1afd006911dd84d7 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Sun, 21 Aug 2022 01:54:37 -0700 Subject: [PATCH 083/143] refac(calcdex): added abilityToggleable in PokeMoves --- src/pages/Calcdex/PokeInfo.tsx | 40 ++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/pages/Calcdex/PokeInfo.tsx b/src/pages/Calcdex/PokeInfo.tsx index 3730c1ef..cb4810f1 100644 --- a/src/pages/Calcdex/PokeInfo.tsx +++ b/src/pages/Calcdex/PokeInfo.tsx @@ -13,11 +13,11 @@ import { FormatLabels, PokemonCommonNatures, PokemonNatureBoosts, - PokemonToggleAbilities, + // PokemonToggleAbilities, } from '@showdex/consts'; import { openSmogonUniversity } from '@showdex/utils/app'; import { detectToggledAbility } from '@showdex/utils/battle'; -import { calcPokemonHp, calcPokemonStats } from '@showdex/utils/calc'; +import { calcPokemonHp, calcPokemonSpreadStats } from '@showdex/utils/calc'; import type { AbilityName, Generation, @@ -28,6 +28,14 @@ import type { CalcdexPokemon, CalcdexPokemonPreset } from '@showdex/redux/store' import { usePresets } from './usePresets'; import styles from './PokeInfo.module.scss'; +type PokeInfoPresetOptions = { + label: string; + options: { + label: string; + value: string; + }[]; +}[]; + export interface PokeInfoProps { className?: string; style?: React.CSSProperties; @@ -49,13 +57,13 @@ export const PokeInfo = ({ }: PokeInfoProps): JSX.Element => { const colorScheme = useColorScheme(); - const find = usePresets({ + const findPresets = usePresets({ format, }); const downloadedSets = React.useMemo( - () => (pokemon?.speciesForme ? find(pokemon.speciesForme, true) : []), - [find, pokemon], + () => (pokemon?.speciesForme ? findPresets(pokemon.speciesForme, true) : []), + [findPresets, pokemon], ); const presets = React.useMemo(() => [ @@ -66,7 +74,7 @@ export const PokeInfo = ({ pokemon?.presets, ]); - const presetOptions = presets.reduce<({ label: string; options: ({ label: string; value: string; })[]; })[]>((options, preset) => { + const presetOptions = presets.reduce((options, preset) => { const genlessFormat = preset.format.replace(`gen${gen}`, ''); const groupLabel = FormatLabels?.[genlessFormat] || genlessFormat; const group = options.find((option) => option.label === groupLabel); @@ -126,7 +134,7 @@ export const PokeInfo = ({ // calculate the stats with the EVs/IVs if (typeof dex?.stats?.calc === 'function') { - mutation.calculatedStats = calcPokemonStats(dex, { + mutation.spreadStats = calcPokemonSpreadStats(dex, { ...pokemon, ...mutation, }); @@ -154,7 +162,7 @@ export const PokeInfo = ({ const pokemonKey = pokemon?.calcdexId || pokemon?.name || '???'; const friendlyPokemonName = pokemon?.speciesForme || pokemon?.name || pokemonKey; - const currentHp = calcPokemonHp(pokemon); + const hpPercentage = calcPokemonHp(pokemon); return (
{ - !!currentHp && - + !!hpPercentage && + {' '} - {`${(currentHp * 100).toFixed(0)}%`} + {`${(hpPercentage * 100).toFixed(0)}%`} } { - (!!pokemon && (!!pokemon.status || pokemon.fainted || !currentHp)) && + (!!pokemon && (!!pokemon.status || pokemon.fainted || !hpPercentage)) && {' '} } @@ -311,7 +319,7 @@ export const PokeInfo = ({ Ability { - PokemonToggleAbilities.includes(pokemon?.dirtyAbility ?? pokemon?.ability) && + pokemon?.abilityToggleable && <> {' '}
+
+ Presents +
+ +
+ Showdex +
+
+ {/* v{env('package-version', '#.#.#')} */} + {printBuildInfo()?.replace(`${env('package-name', 'lol')}-`, '')} +
+ + {/*
*/} +
+ +
+
+ window.open(forumUrl, '_blank', 'noopener,noreferrer')} + > + + Forum + + + + Post + + +
+ +
+ window.open(githubUrl, '_blank', 'noopener,noreferrer')} + > + + Git + + + + Hub + + +
+ + +
+ +
+

+ If you enjoyed this extension, + please consider donating to support further development. +

+

+ We created this as a passion project for the community, + and we hope it helps you in your battles! +

+ + window.open(donationUrl, '_blank', 'noopener,noreferrer')} + > + + Donate + + + via + + + +
+ +
+ window.open('https://tize.io', '_blank')} + > + + + +
created with ♥ by
+
sumfuk/doshidak & camdawgboi
+
+ + + ); +}; diff --git a/src/pages/Hellodex/index.ts b/src/pages/Hellodex/index.ts new file mode 100644 index 00000000..3c4508de --- /dev/null +++ b/src/pages/Hellodex/index.ts @@ -0,0 +1 @@ +export * from './Hellodex.bootstrap'; diff --git a/src/pages/index.ts b/src/pages/index.ts index 83d143d9..4ea31329 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -1 +1,2 @@ export * from './Calcdex'; +export * from './Hellodex'; From 5739187c2479e169e566ad3270591759225cb9de Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Wed, 31 Aug 2022 05:17:30 -0700 Subject: [PATCH 139/143] chore: cleaned up build script a bit --- scripts/build.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/build.js b/scripts/build.js index 378d89c7..e4eba617 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -1,12 +1,13 @@ import webpack from 'webpack'; import { config } from '../webpack.config'; +// note: this doesn't apply to the webpack config since it's imported before process.env.BABEL_ENV = 'production'; process.env.NODE_ENV = 'production'; -if ('chromeExtension' in config) { - delete config.chromeExtension; -} +// if ('chromeExtension' in config) { +// delete config.chromeExtension; +// } config.mode = 'production'; From 7e484b7606db3b0033337d360d772109a2c38c9d Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Wed, 31 Aug 2022 05:31:43 -0700 Subject: [PATCH 140/143] chore: installed rimraf --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 344e00eb..c74f5e52 100644 --- a/package.json +++ b/package.json @@ -112,6 +112,7 @@ "html-webpack-plugin": "^5.5.0", "postcss": "^8.4.16", "postcss-loader": "^7.0.1", + "rimraf": "^3.0.2", "sass": "^1.54.3", "sass-loader": "^13.0.2", "source-map-loader": "^4.0.0", From 5a5569c2784628e6a7897af66d531a812598aadb Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Wed, 31 Aug 2022 05:36:22 -0700 Subject: [PATCH 141/143] chore: added clean scripts --- package.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c74f5e52..3f4206db 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,11 @@ "commit": "./node_modules/cz-customizable/standalone.js", "cm": "yarn commit", "cz": "yarn commit", - "dev": "NODE_ENV=development node --conditions=development --experimental-specifier-resolution=node --trace-warnings -- ./scripts/dev.js", - "build": "NODE_ENV=production node --conditions=production --experimental-specifier-resolution=node -- ./scripts/build.js" + "clean:dev": "yarn rimraf ./build", + "clean:prod": "yarn rimraf ./dist", + "clean": "yarn clean:dev && yarn clean:prod", + "dev": "yarn clean:dev && NODE_ENV=development node --conditions=development --experimental-specifier-resolution=node --trace-warnings -- ./scripts/dev.js", + "build": "yarn clean:prod && NODE_ENV=production node --conditions=production --experimental-specifier-resolution=node -- ./scripts/build.js" }, "config": { "cz-customizable": { From cec94d34b4e776f6d683315eacbdb751884b86a4 Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Wed, 31 Aug 2022 05:38:05 -0700 Subject: [PATCH 142/143] chore: specified babel importAssertions in eslint config since our target node version supports import assertions, we don't need a babel plugin, so this is how we tell eslint it's trippin --- .eslintrc.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.eslintrc.json b/.eslintrc.json index 8352c21d..568821bb 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -17,7 +17,15 @@ "parser": "@typescript-eslint/parser", "parserOptions": { - "project": "./tsconfig.json" + "project": "./tsconfig.json", + + "babelOptions": { + "parserOpts": { + "plugins": [ + "importAssertions" + ] + } + } }, "plugins": [ From 44e0e25fa7203ef5bdbb684d3766ca13b396145c Mon Sep 17 00:00:00 2001 From: Keith Choison Date: Wed, 31 Aug 2022 05:39:24 -0700 Subject: [PATCH 143/143] chore: added asset copy patterns from manifest in webpack --- webpack.config.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/webpack.config.js b/webpack.config.js index 45978df8..5172cb9b 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -5,6 +5,7 @@ import webpack from 'webpack'; import CopyWebpackPlugin from 'copy-webpack-plugin'; import HtmlWebpackPlugin from 'html-webpack-plugin'; import TerserPlugin from 'terser-webpack-plugin'; +import manifest from './src/manifest.json' assert { type: 'json' }; const __DEV__ = process.env.NODE_ENV !== 'production'; const mode = __DEV__ ? 'development' : 'production'; @@ -125,7 +126,11 @@ const copyPatterns = [{ }, { from: 'src/assets/**/*', to: '[name][ext]', - filter: (path) => moduleRules[1].test.test(path), + // filter: (path) => moduleRules[1].test.test(path), + filter: (path) => moduleRules[1].test.test(path) && [ + ...manifest.web_accessible_resources.flatMap((r) => r.resources), + ...Object.values(manifest.icons), + ].some((name) => path.includes(name)), }]; const plugins = [