diff --git a/src/@types/enum-types.ts b/src/@types/enum-types.ts new file mode 100644 index 00000000000..eb2145ed71f --- /dev/null +++ b/src/@types/enum-types.ts @@ -0,0 +1,54 @@ +/** Union type accepting any TS Enum or `const object`, with or without reverse mapping. */ +export type EnumOrObject = Record; + +/** + * Utility type to extract the enum values from a `const object`, + * or convert an `enum` object produced by `typeof Enum` into its literal value union. + */ +export type EnumValues = E[keyof E]; + +/** + * Generic type constraint representing a TS numeric enum with reverse mappings. + * @example + * TSNumericEnum + */ +// NB: this works because `EnumValues` returns the underlying Enum union type. +export type TSNumericEnum = number extends EnumValues ? T : never; + +/** Generic type constraint representing a non reverse-mapped TS enum or `const object`. */ +export type NormalEnum = Exclude>; + +// ### Type check tests + +enum testEnumNum { + testN1 = 1, + testN2 = 2, +} + +enum testEnumString { + testS1 = "apple", + testS2 = "banana", +} + +const testObjNum = { testON1: 1, testON2: 2 } as const; + +const testObjString = { testOS1: "apple", testOS2: "banana" } as const; + +testEnumNum satisfies EnumOrObject; +testEnumString satisfies EnumOrObject; +testObjNum satisfies EnumOrObject; +testObjString satisfies EnumOrObject; + +// @ts-expect-error - This is intentionally supposed to fail as an example +testEnumNum satisfies NormalEnum; +testEnumString satisfies NormalEnum; +testObjNum satisfies NormalEnum; +testObjString satisfies NormalEnum; + +testEnumNum satisfies TSNumericEnum; +// @ts-expect-error - This is intentionally supposed to fail as an example +testEnumString satisfies TSNumericEnum; +// @ts-expect-error - This is intentionally supposed to fail as an example +testObjNum satisfies TSNumericEnum; +// @ts-expect-error - This is intentionally supposed to fail as an example +testObjString satisfies TSNumericEnum; diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 784c3ce8334..301e054ea2f 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -10,7 +10,6 @@ import { fixedInt, getIvsFromId, randSeedInt, - getEnumValues, randomString, NumberHolder, shiftCharCodes, @@ -19,6 +18,7 @@ import { BooleanHolder, type Constructor, } from "#app/utils/common"; +import { getEnumValues } from "./utils/enums"; import { deepMergeSpriteData } from "#app/utils/data"; import type { Modifier, ModifierPredicate, TurnHeldItemTransferModifier } from "./modifier/modifier"; import { diff --git a/src/battle.ts b/src/battle.ts index 45373402e12..4b6b776058f 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -2,7 +2,6 @@ import { globalScene } from "#app/global-scene"; import type { Command } from "#enums/command"; import { randomString, - getEnumValues, NumberHolder, randSeedInt, shiftCharCodes, @@ -10,6 +9,7 @@ import { randInt, randSeedFloat, } from "#app/utils/common"; +import { getEnumValues } from "./utils/enums"; import Trainer from "./field/trainer"; import { TrainerVariant } from "#enums/trainer-variant"; import type { GameMode } from "./game-mode"; diff --git a/src/data/balance/biomes.ts b/src/data/balance/biomes.ts index 713ab9637ab..70186a1ed87 100644 --- a/src/data/balance/biomes.ts +++ b/src/data/balance/biomes.ts @@ -1,5 +1,6 @@ import { PokemonType } from "#enums/pokemon-type"; -import { randSeedInt, getEnumValues } from "#app/utils/common"; +import { randSeedInt } from "#app/utils/common"; +import { getEnumValues } from "#app/utils/enums"; import type { SpeciesFormEvolution } from "#app/data/balance/pokemon-evolutions"; import { pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; import i18next from "i18next"; @@ -7708,10 +7709,10 @@ export function initBiomes() { const traverseBiome = (biome: BiomeId, depth: number) => { if (biome === BiomeId.END) { - const biomeList = Object.keys(BiomeId).filter(key => !Number.isNaN(Number(key))); + const biomeList = getEnumValues(BiomeId) biomeList.pop(); // Removes BiomeId.END from the list const randIndex = randSeedInt(biomeList.length, 1); // Will never be BiomeId.TOWN - biome = BiomeId[biomeList[randIndex]]; + biome = biomeList[randIndex]; } const linkedBiomes: (BiomeId | [ BiomeId, number ])[] = Array.isArray(biomeLinks[biome]) ? biomeLinks[biome] as (BiomeId | [ BiomeId, number ])[] diff --git a/src/data/balance/egg-moves.ts b/src/data/balance/egg-moves.ts index fa89e558ba7..1c5e6714711 100644 --- a/src/data/balance/egg-moves.ts +++ b/src/data/balance/egg-moves.ts @@ -1,8 +1,8 @@ import { allMoves } from "../data-lists"; -import { getEnumKeys, getEnumValues } from "#app/utils/common"; +import { getEnumKeys, getEnumValues } from "#app/utils/enums"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; - +import { toReadableString } from "#app/utils/common"; export const speciesEggMoves = { [SpeciesId.BULBASAUR]: [ MoveId.SAPPY_SEED, MoveId.MALIGNANT_CHAIN, MoveId.EARTH_POWER, MoveId.MATCHA_GOTCHA ], @@ -584,17 +584,24 @@ export const speciesEggMoves = { [SpeciesId.BLOODMOON_URSALUNA]: [ MoveId.NASTY_PLOT, MoveId.ROCK_POLISH, MoveId.SANDSEAR_STORM, MoveId.BOOMBURST ] }; +/** + * Parse a CSV-separated list of Egg Moves (such as one sourced from a Google Sheets) + * into code able to form the `speciesEggMoves` const object as above. + * @param content - The CSV-formatted string to convert into code. + */ +// TODO: Move this into the scripts folder and stop running it on initialization function parseEggMoves(content: string): void { let output = ""; const speciesNames = getEnumKeys(SpeciesId); const speciesValues = getEnumValues(SpeciesId); + const moveNames = allMoves.map(m => m.name.replace(/ \([A-Z]\)$/, "").toLowerCase()); const lines = content.split(/\n/g); for (const line of lines) { const cols = line.split(",").slice(0, 5); - const moveNames = allMoves.map(m => m.name.replace(/ \([A-Z]\)$/, "").toLowerCase()); - const enumSpeciesName = cols[0].toUpperCase().replace(/[ -]/g, "_"); + const enumSpeciesName = cols[0].toUpperCase().replace(/[ -]/g, "_") as keyof typeof SpeciesId; + // TODO: This should use reverse mapping instead of `indexOf` const species = speciesValues[speciesNames.indexOf(enumSpeciesName)]; const eggMoves: MoveId[] = []; @@ -602,14 +609,16 @@ function parseEggMoves(content: string): void { for (let m = 0; m < 4; m++) { const moveName = cols[m + 1].trim(); const moveIndex = moveName !== "N/A" ? moveNames.indexOf(moveName.toLowerCase()) : -1; - eggMoves.push(moveIndex > -1 ? moveIndex as MoveId : MoveId.NONE); - if (moveIndex === -1) { console.warn(moveName, "could not be parsed"); } + + eggMoves.push(moveIndex > -1 ? moveIndex as MoveId : MoveId.NONE); } - if (eggMoves.find(m => m !== MoveId.NONE)) { + if (eggMoves.every(m => m === MoveId.NONE)) { + console.warn(`Species ${toReadableString(SpeciesId[species])} could not be parsed, excluding from output...`) + } else { output += `[SpeciesId.${SpeciesId[species]}]: [ ${eggMoves.map(m => `MoveId.${MoveId[m]}`).join(", ")} ],\n`; } } diff --git a/src/data/battle-anims.ts b/src/data/battle-anims.ts index bed17fb0ebc..f0055bc9fdc 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -2,15 +2,8 @@ import { globalScene } from "#app/global-scene"; import { allMoves } from "#app/data/data-lists"; import { MoveFlags } from "#enums/MoveFlags"; import type Pokemon from "#app/field/pokemon"; -import { - type nil, - getFrameMs, - getEnumKeys, - getEnumValues, - animationFileName, - coerceArray, - isNullOrUndefined, -} from "#app/utils/common"; +import { type nil, getFrameMs, animationFileName, coerceArray, isNullOrUndefined } from "#app/utils/common"; +import { getEnumKeys, getEnumValues } from "#app/utils/enums"; import type { BattlerIndex } from "#enums/battler-index"; import { MoveId } from "#enums/move-id"; import Phaser from "phaser"; @@ -1402,10 +1395,10 @@ export class EncounterBattleAnim extends BattleAnim { export async function populateAnims() { const commonAnimNames = getEnumKeys(CommonAnim).map(k => k.toLowerCase()); const commonAnimMatchNames = commonAnimNames.map(k => k.replace(/_/g, "")); - const commonAnimIds = getEnumValues(CommonAnim) as CommonAnim[]; + const commonAnimIds = getEnumValues(CommonAnim); const chargeAnimNames = getEnumKeys(ChargeAnim).map(k => k.toLowerCase()); const chargeAnimMatchNames = chargeAnimNames.map(k => k.replace(/_/g, " ")); - const chargeAnimIds = getEnumValues(ChargeAnim) as ChargeAnim[]; + const chargeAnimIds = getEnumValues(ChargeAnim); const commonNamePattern = /name: (?:Common:)?(Opp )?(.*)/; const moveNameToId = {}; for (const move of getEnumValues(MoveId).slice(1)) { diff --git a/src/data/daily-run.ts b/src/data/daily-run.ts index e869ba7f28f..8bf7c5c828a 100644 --- a/src/data/daily-run.ts +++ b/src/data/daily-run.ts @@ -3,7 +3,8 @@ import type { SpeciesId } from "#enums/species-id"; import { globalScene } from "#app/global-scene"; import { PlayerPokemon } from "#app/field/pokemon"; import type { Starter } from "#app/ui/starter-select-ui-handler"; -import { randSeedGauss, randSeedInt, randSeedItem, getEnumValues } from "#app/utils/common"; +import { randSeedGauss, randSeedInt, randSeedItem } from "#app/utils/common"; +import { getEnumValues } from "#app/utils/enums"; import type { PokemonSpeciesForm } from "#app/data/pokemon-species"; import PokemonSpecies, { getPokemonSpeciesForm } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils"; diff --git a/src/data/dialogue.ts b/src/data/dialogue.ts index 6bb96f0efb2..cc023a24ab0 100644 --- a/src/data/dialogue.ts +++ b/src/data/dialogue.ts @@ -1744,6 +1744,7 @@ export function getCharVariantFromDialogue(message: string): string { } export function initTrainerTypeDialogue(): void { + // TODO: this should not be using `Object.Keys` const trainerTypes = Object.keys(trainerTypeDialogue).map(t => Number.parseInt(t) as TrainerType); for (const trainerType of trainerTypes) { const messages = trainerTypeDialogue[trainerType]; diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index f94c59bb463..aa91a1c8179 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -27,7 +27,8 @@ import { } from "../status-effect"; import { getTypeDamageMultiplier } from "../type"; import { PokemonType } from "#enums/pokemon-type"; -import { BooleanHolder, NumberHolder, isNullOrUndefined, toDmgValue, randSeedItem, randSeedInt, getEnumValues, toReadableString, type Constructor, randSeedFloat } from "#app/utils/common"; +import { BooleanHolder, NumberHolder, isNullOrUndefined, toDmgValue, randSeedItem, randSeedInt, toReadableString, type Constructor, randSeedFloat } from "#app/utils/common"; +import { getEnumValues } from "#app/utils/enums"; import { WeatherType } from "#enums/weather-type"; import type { ArenaTrapTag } from "../arena-tag"; import { WeakenMoveTypeTag } from "../arena-tag"; diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 5b88ae0867b..f46dca33751 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -25,7 +25,6 @@ import { BooleanHolder, randSeedItem, isNullOrUndefined, - getEnumValues, toDmgValue, fixedInt, rgbaToInt, @@ -38,6 +37,7 @@ import { randSeedIntRange, coerceArray, } from "#app/utils/common"; +import { getEnumValues } from "#app/utils/enums"; import type { TypeDamageMultiplier } from "#app/data/type"; import { getTypeDamageMultiplier, getTypeRgb } from "#app/data/type"; import { PokemonType } from "#enums/pokemon-type"; diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 388802f467e..8f8c1a96fc0 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -1,5 +1,5 @@ import Phaser from "phaser"; -import { getEnumValues } from "#app/utils/common"; +import { getEnumValues } from "./utils/enums"; import { deepCopy } from "#app/utils/data"; import pad_generic from "./configs/inputs/pad_generic"; import pad_unlicensedSNES from "./configs/inputs/pad_unlicensedSNES"; diff --git a/src/loading-scene.ts b/src/loading-scene.ts index f67d19e1027..0c242b7344a 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -4,7 +4,8 @@ import CacheBustedLoaderPlugin from "#app/plugins/cache-busted-loader-plugin"; import { SceneBase } from "#app/scene-base"; import { WindowVariant, getWindowVariantSuffix } from "#app/ui/ui-theme"; import { isMobile } from "#app/touch-controls"; -import { localPing, getEnumValues, hasAllLocalizedSprites, getEnumKeys } from "#app/utils/common"; +import { localPing, hasAllLocalizedSprites } from "#app/utils/common"; +import { getEnumValues, getEnumKeys } from "./utils/enums"; import { initPokemonPrevolutions, initPokemonStarters } from "#app/data/balance/pokemon-evolutions"; import { initBiomes } from "#app/data/balance/biomes"; import { initEggMoves } from "#app/data/balance/egg-moves"; diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index fcbe6b66a4e..6c5be4399ce 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -103,15 +103,8 @@ import { getVoucherTypeIcon, getVoucherTypeName, VoucherType } from "#app/system import type { PokemonMoveSelectFilter, PokemonSelectFilter } from "#app/ui/party-ui-handler"; import PartyUiHandler from "#app/ui/party-ui-handler"; import { getModifierTierTextTint } from "#app/ui/text"; -import { - formatMoney, - getEnumKeys, - getEnumValues, - isNullOrUndefined, - NumberHolder, - padInt, - randSeedInt, -} from "#app/utils/common"; +import { formatMoney, isNullOrUndefined, NumberHolder, padInt, randSeedInt } from "#app/utils/common"; +import { getEnumKeys, getEnumValues } from "#app/utils/enums"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; import { MoveId } from "#enums/move-id"; diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 2e94b085948..2df82caccdd 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -19,7 +19,8 @@ import { MoveResult } from "#enums/move-result"; import { getPokemonNameWithAffix } from "#app/messages"; import Overrides from "#app/overrides"; import { BattlePhase } from "#app/phases/battle-phase"; -import { enumValueToKey, NumberHolder } from "#app/utils/common"; +import { NumberHolder } from "#app/utils/common"; +import { enumValueToKey } from "#app/utils/enums"; import { AbilityId } from "#enums/ability-id"; import { ArenaTagType } from "#enums/arena-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type"; diff --git a/src/system/game-data.ts b/src/system/game-data.ts index d5d4256f7d0..0c6c6decd14 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -9,7 +9,8 @@ import type PokemonSpecies from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { allSpecies } from "#app/data/data-lists"; import { speciesStarterCosts } from "#app/data/balance/starters"; -import { randInt, getEnumKeys, isLocal, executeIf, fixedInt, randSeedItem, NumberHolder } from "#app/utils/common"; +import { randInt, isLocal, executeIf, fixedInt, randSeedItem, NumberHolder } from "#app/utils/common"; +import { getEnumKeys } from "#app/utils/enums"; import Overrides from "#app/overrides"; import PokemonData from "#app/system/pokemon-data"; import PersistentModifierData from "#app/system/modifier-data"; diff --git a/src/ui/daily-run-scoreboard.ts b/src/ui/daily-run-scoreboard.ts index c069c6fffd7..4cf4e924182 100644 --- a/src/ui/daily-run-scoreboard.ts +++ b/src/ui/daily-run-scoreboard.ts @@ -1,6 +1,7 @@ import i18next from "i18next"; import { globalScene } from "#app/global-scene"; -import { getEnumKeys, executeIf } from "#app/utils/common"; +import { executeIf } from "#app/utils/common"; +import { getEnumKeys } from "#app/utils/enums"; import { TextStyle, addTextObject } from "./text"; import { WindowVariant, addWindow } from "./ui-theme"; import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; diff --git a/src/ui/egg-gacha-ui-handler.ts b/src/ui/egg-gacha-ui-handler.ts index 7ff3a1b65ee..d99671157bc 100644 --- a/src/ui/egg-gacha-ui-handler.ts +++ b/src/ui/egg-gacha-ui-handler.ts @@ -1,7 +1,8 @@ import { UiMode } from "#enums/ui-mode"; import { TextStyle, addTextObject, getEggTierTextTint, getTextStyleOptions } from "./text"; import MessageUiHandler from "./message-ui-handler"; -import { getEnumValues, getEnumKeys, fixedInt, randSeedShuffle } from "#app/utils/common"; +import { fixedInt, randSeedShuffle } from "#app/utils/common"; +import { getEnumValues, getEnumKeys } from "#app/utils/enums"; import type { IEggOptions } from "../data/egg"; import { Egg, getLegendaryGachaSpeciesForTimestamp } from "../data/egg"; import { VoucherType, getVoucherTypeIcon } from "../system/voucher"; @@ -299,7 +300,7 @@ export default class EggGachaUiHandler extends MessageUiHandler { this.eggGachaContainer.add(this.eggGachaOptionsContainer); - new Array(getEnumKeys(VoucherType).length).fill(null).map((_, i) => { + getEnumValues(VoucherType).forEach((voucher, i) => { const container = globalScene.add.container(globalScene.game.canvas.width / 6 - 56 * i, 0); const bg = addWindow(0, 0, 56, 22); @@ -312,7 +313,7 @@ export default class EggGachaUiHandler extends MessageUiHandler { this.voucherCountLabels.push(countLabel); - const iconImage = getVoucherTypeIcon(i as VoucherType); + const iconImage = getVoucherTypeIcon(voucher); const icon = globalScene.add.sprite(-19, 2, "items", iconImage); icon.setOrigin(0, 0); diff --git a/src/ui/menu-ui-handler.ts b/src/ui/menu-ui-handler.ts index 5ab1d4f9e96..52cdba140a3 100644 --- a/src/ui/menu-ui-handler.ts +++ b/src/ui/menu-ui-handler.ts @@ -2,7 +2,8 @@ import { bypassLogin } from "#app/global-vars/bypass-login"; import { globalScene } from "#app/global-scene"; import { TextStyle, addTextObject, getTextStyleOptions } from "./text"; import { UiMode } from "#enums/ui-mode"; -import { getEnumKeys, isLocal, fixedInt, sessionIdKey } from "#app/utils/common"; +import { isLocal, fixedInt, sessionIdKey } from "#app/utils/common"; +import { getEnumValues } from "#app/utils/enums"; import { isBeta } from "#app/utils/utility-vars"; import { getCookie } from "#app/utils/cookies"; import { addWindow, WindowVariant } from "./ui-theme"; @@ -76,11 +77,9 @@ export default class MenuUiHandler extends MessageUiHandler { { condition: bypassLogin, options: [MenuOptions.LOG_OUT] }, ]; - this.menuOptions = getEnumKeys(MenuOptions) - .map(m => Number.parseInt(MenuOptions[m]) as MenuOptions) - .filter(m => { - return !this.excludedMenus().some(exclusion => exclusion.condition && exclusion.options.includes(m)); - }); + this.menuOptions = getEnumValues(MenuOptions).filter(m => { + return !this.excludedMenus().some(exclusion => exclusion.condition && exclusion.options.includes(m)); + }); } setup(): void { @@ -131,11 +130,9 @@ export default class MenuUiHandler extends MessageUiHandler { { condition: bypassLogin, options: [MenuOptions.LOG_OUT] }, ]; - this.menuOptions = getEnumKeys(MenuOptions) - .map(m => Number.parseInt(MenuOptions[m]) as MenuOptions) - .filter(m => { - return !this.excludedMenus().some(exclusion => exclusion.condition && exclusion.options.includes(m)); - }); + this.menuOptions = getEnumValues(MenuOptions).filter(m => { + return !this.excludedMenus().some(exclusion => exclusion.condition && exclusion.options.includes(m)); + }); this.optionSelectText = addTextObject( 0, @@ -511,11 +508,9 @@ export default class MenuUiHandler extends MessageUiHandler { this.render(); super.show(args); - this.menuOptions = getEnumKeys(MenuOptions) - .map(m => Number.parseInt(MenuOptions[m]) as MenuOptions) - .filter(m => { - return !this.excludedMenus().some(exclusion => exclusion.condition && exclusion.options.includes(m)); - }); + this.menuOptions = getEnumValues(MenuOptions).filter(m => { + return !this.excludedMenus().some(exclusion => exclusion.condition && exclusion.options.includes(m)); + }); this.menuContainer.setVisible(true); this.setCursor(0); diff --git a/src/ui/pokedex-page-ui-handler.ts b/src/ui/pokedex-page-ui-handler.ts index 9ec74e70b23..bb883d6670a 100644 --- a/src/ui/pokedex-page-ui-handler.ts +++ b/src/ui/pokedex-page-ui-handler.ts @@ -58,7 +58,7 @@ import { toReadableString, } from "#app/utils/common"; import type { Nature } from "#enums/nature"; -import { getEnumKeys } from "#app/utils/common"; +import { getEnumValues } from "#app/utils/enums"; import { speciesTmMoves } from "#app/data/balance/tms"; import type { BiomeTierTod } from "#app/data/balance/biomes"; import { BiomePoolTier, catchableSpecies } from "#app/data/balance/biomes"; @@ -603,7 +603,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler { this.menuContainer.setVisible(false); - this.menuOptions = getEnumKeys(MenuOptions).map(m => Number.parseInt(MenuOptions[m]) as MenuOptions); + this.menuOptions = getEnumValues(MenuOptions); this.optionSelectText = addBBCodeTextObject( 0, @@ -707,7 +707,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler { this.starterAttributes = this.initStarterPrefs(); - this.menuOptions = getEnumKeys(MenuOptions).map(m => Number.parseInt(MenuOptions[m]) as MenuOptions); + this.menuOptions = getEnumValues(MenuOptions); this.menuContainer.setVisible(true); diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index f108faf1646..6a9418c2587 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -6,13 +6,13 @@ import { getLocalizedSpriteKey, rgbHexToRgba, padInt, - getEnumValues, fixedInt, isNullOrUndefined, toReadableString, formatStat, getShinyDescriptor, } from "#app/utils/common"; +import { getEnumValues } from "#app/utils/enums"; import type { PlayerPokemon } from "#app/field/pokemon"; import type { PokemonMove } from "#app/data/moves/pokemon-move"; import { getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters"; diff --git a/src/utils/common.ts b/src/utils/common.ts index 4bf51730148..3f13d40c82c 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -8,11 +8,18 @@ export type nil = null | undefined; export const MissingTextureKey = "__MISSING"; +// TODO: Draft tests for these utility functions +/** + * Convert a `snake_case` string in any capitalization (such as one from an enum reverse mapping) + * into a readable `Title Case` version. + * @param str - The snake case string to be converted. + * @returns The result of converting `str` into title case. + */ export function toReadableString(str: string): string { return str .replace(/_/g, " ") .split(" ") - .map(s => `${s.slice(0, 1)}${s.slice(1).toLowerCase()}`) + .map(s => capitalizeFirstLetter(s.toLowerCase())) .join(" "); } @@ -257,18 +264,6 @@ export function formatStat(stat: number, forHp = false): string { return formatLargeNumber(stat, forHp ? 100000 : 1000000); } -export function getEnumKeys(enumType: any): string[] { - return Object.values(enumType) - .filter(v => Number.isNaN(Number.parseInt(v!.toString()))) - .map(v => v!.toString()); -} - -export function getEnumValues(enumType: any): number[] { - return Object.values(enumType) - .filter(v => !Number.isNaN(Number.parseInt(v!.toString()))) - .map(v => Number.parseInt(v!.toString())); -} - export function executeIf(condition: boolean, promiseFunc: () => Promise): Promise { return condition ? promiseFunc() : new Promise(resolve => resolve(null)); } @@ -620,25 +615,3 @@ export function coerceArray(input: T): T extends any[] ? T : [T]; export function coerceArray(input: T): T | [T] { return Array.isArray(input) ? input : [input]; } - -/** - * Returns the name of the key that matches the enum [object] value. - * @param input - The enum [object] to check - * @param val - The value to get the key of - * @returns The name of the key with the specified value - * @example - * const thing = { - * one: 1, - * two: 2, - * } as const; - * console.log(enumValueToKey(thing, thing.two)); // output: "two" - * @throws An `Error` if an invalid enum value is passed to the function - */ -export function enumValueToKey>(input: T, val: T[keyof T]): keyof T { - for (const [key, value] of Object.entries(input)) { - if (val === value) { - return key as keyof T; - } - } - throw new Error(`Invalid value passed to \`enumValueToKey\`! Value: ${val}`); -} diff --git a/src/utils/enums.ts b/src/utils/enums.ts new file mode 100644 index 00000000000..dcc3ce1d335 --- /dev/null +++ b/src/utils/enums.ts @@ -0,0 +1,51 @@ +// biome-ignore lint/correctness/noUnusedImports: Used for a JSDoc comment +import type { EnumOrObject, EnumValues, TSNumericEnum, NormalEnum } from "#app/@types/enum-types"; + +/** + * Return the string keys of an Enum object, excluding reverse-mapped numbers. + * @param enumType - The numeric enum to retrieve keys for. + * @returns An ordered array of all of `enumType`'s string keys. + * @remarks + * To retrieve the keys of a {@linkcode NormalEnum}, use {@linkcode Object.keys} instead. + */ +export function getEnumKeys(enumType: TSNumericEnum): (keyof E)[] { + // All enum values are either normal or reverse mapped, so we can retrieve the keys by filtering out all strings. + return Object.values(enumType).filter(v => typeof v === "string"); +} + +/** + * Return the numeric values of a numeric Enum object, excluding reverse-mapped strings. + * @param enumType - The enum object to retrieve keys for. + * @returns An ordered array of all of `enumType`'s number values. + * @remarks + * To retrieve the keys of a {@linkcode NormalEnum}, use {@linkcode Object.values} instead. + */ +// NB: This does not use `EnumValues` due to variable highlighting in IDEs. +export function getEnumValues(enumType: TSNumericEnum): E[keyof E][] { + return Object.values(enumType).filter(v => typeof v !== "string") as E[keyof E][]; +} + +/** + * Return the name of the key that matches the given Enum value. + * Can be used to emulate Typescript reverse mapping for `const object`s or string enums. + * @param object - The {@linkcode EnumOrObject} to check + * @param val - The value to get the key of. + * @returns The name of the key with the specified value. + * @example + * const thing = { + * one: 1, + * two: 2, + * } as const; + * console.log(enumValueToKey(thing, thing.two)); // output: "two" + * @throws Error if an invalid enum value is passed to the function + * @remarks + * If multiple keys map to the same value, the first one (in insertion order) will be retrieved. + */ +export function enumValueToKey(object: T, val: T[keyof T]): keyof T { + for (const [key, value] of Object.entries(object)) { + if (val === value) { + return key as keyof T; + } + } + throw new Error(`Invalid value passed to \`enumValueToKey\`! Value: ${val}`); +}