diff --git a/src/@types/enum-types.ts b/src/@types/enum-types.ts new file mode 100644 index 00000000000..84df0a96505 --- /dev/null +++ b/src/@types/enum-types.ts @@ -0,0 +1,18 @@ +/** 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` interface produced by `typeof Enum` into the union type representing its values. + */ +export type EnumValues = E[keyof E]; + +/** + * Generic type constraint representing a TS numeric enum with reverse mappings. + * @example + * TSNumericEnum + */ +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>; diff --git a/src/@types/type-helpers.ts b/src/@types/type-helpers.ts index 2d00b1faf4a..375d8f64de3 100644 --- a/src/@types/type-helpers.ts +++ b/src/@types/type-helpers.ts @@ -2,8 +2,10 @@ * A collection of custom utility types that aid in type checking and ensuring strict type conformity */ -// biome-ignore lint/correctness/noUnusedImports: Used in a tsdoc comment +// biome-ignore-start lint/correctness/noUnusedImports: Used in a tsdoc comment +import type { EnumValues } from "#app/@types/enum-types"; import type { AbAttr } from "./ability-types"; +// biome-ignore-end lint/correctness/noUnusedImports: Used in a tsdoc comment /** * Exactly matches the type of the argument, preventing adding additional properties. @@ -11,7 +13,7 @@ import type { AbAttr } from "./ability-types"; * ⚠️ Should never be used with `extends`, as this will nullify the exactness of the type. * * As an example, used to ensure that the parameters of {@linkcode AbAttr.canApply} and {@linkcode AbAttr.getTriggerMessage} are compatible with - * the type of the apply method + * the type of its {@linkcode AbAttr.apply | apply} method. * * @typeParam T - The type to match exactly */ @@ -26,9 +28,18 @@ export type Exact = { export type Closed = X; /** - * Remove `readonly` from all properties of the provided type - * @typeParam T - The type to make mutable + * Remove `readonly` from all properties of the provided type. + * @typeParam T - The type to make mutable. */ export type Mutable = { -readonly [P in keyof T]: T[P]; }; + +/** + * Type helper to obtain the keys associated with a given value inside a `const object`. + * @typeParam O - The type of the object + * @typeParam V - The type of one of O's values + */ +export type InferKeys, V extends EnumValues> = { + [K in keyof O]: O[K] extends V ? K : never; +}[keyof O]; diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 59c0e28422b..618140c0c18 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -10,7 +10,6 @@ import { fixedInt, getIvsFromId, randSeedInt, - getEnumValues, randomString, NumberHolder, shiftCharCodes, @@ -20,6 +19,7 @@ import { type Constructor, isBetween, } from "#app/utils/common"; +import { getEnumValues } from "./utils/enums"; import { deepMergeSpriteData } from "#app/utils/data"; import type { Modifier, ModifierPredicate, TurnHeldItemTransferModifier } from "./modifier/modifier"; import { @@ -2182,6 +2182,7 @@ export default class BattleScene extends SceneBase { ), ] : allSpecies.filter(s => s.isCatchable()); + // TODO: should this use `randSeedItem`? return filteredSpecies[randSeedInt(filteredSpecies.length)]; } @@ -2207,6 +2208,7 @@ export default class BattleScene extends SceneBase { } } + // TODO: should this use `randSeedItem`? return biomes[randSeedInt(biomes.length)]; } @@ -3726,6 +3728,7 @@ export default class BattleScene extends SceneBase { console.log("No Mystery Encounters found, falling back to Mysterious Challengers."); return allMysteryEncounters[MysteryEncounterType.MYSTERIOUS_CHALLENGERS]; } + // TODO: should this use `randSeedItem`? encounter = availableEncounters[randSeedInt(availableEncounters.length)]; // New encounter object to not dirty flags encounter = new MysteryEncounter(encounter); diff --git a/src/battle.ts b/src/battle.ts index ba4152227dd..905a02aca93 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..960de3497a3 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"; @@ -165,5 +166,6 @@ export function getDailyStartingBiome(): BiomeId { } // Fallback in case something went wrong + // TODO: should this use `randSeedItem`? return biomes[randSeedInt(biomes.length)]; } 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 0878ece2f01..864e4f0e723 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -28,7 +28,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/data/mystery-encounters/encounters/berries-abound-encounter.ts b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts index d86a8439804..e55188fc3c4 100644 --- a/src/data/mystery-encounters/encounters/berries-abound-encounter.ts +++ b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts @@ -15,7 +15,7 @@ import type { BerryModifierType, ModifierTypeOption } from "#app/modifier/modifi import { regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; import { modifierTypes } from "#app/data/data-lists"; import { ModifierPoolType } from "#enums/modifier-pool-type"; -import { randSeedInt } from "#app/utils/common"; +import { randSeedItem } from "#app/utils/common"; import { BattlerTagType } from "#enums/battler-tag-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { globalScene } from "#app/global-scene"; @@ -38,6 +38,7 @@ import i18next from "#app/plugins/i18n"; import { BerryType } from "#enums/berry-type"; import { PERMANENT_STATS, Stat } from "#enums/stat"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; +import { getEnumValues } from "#app/utils/enums"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/berriesAbound"; @@ -311,7 +312,7 @@ export const BerriesAboundEncounter: MysteryEncounter = MysteryEncounterBuilder. .build(); function tryGiveBerry(prioritizedPokemon?: PlayerPokemon) { - const berryType = randSeedInt(Object.keys(BerryType).filter(s => !Number.isNaN(Number(s))).length) as BerryType; + const berryType = randSeedItem(getEnumValues(BerryType)); const berry = generateModifierType(modifierTypes.BERRY, [berryType]) as BerryModifierType; const party = globalScene.getPlayerParty(); diff --git a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts index df06f40c159..8c10a4f2b2f 100644 --- a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts +++ b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts @@ -292,6 +292,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde // Init the moves available for tutor const moveTutorOptions: PokemonMove[] = []; + // TODO: should this use `randSeedItem`? moveTutorOptions.push(new PokemonMove(PHYSICAL_TUTOR_MOVES[randSeedInt(PHYSICAL_TUTOR_MOVES.length)])); moveTutorOptions.push(new PokemonMove(SPECIAL_TUTOR_MOVES[randSeedInt(SPECIAL_TUTOR_MOVES.length)])); moveTutorOptions.push(new PokemonMove(STATUS_TUTOR_MOVES[randSeedInt(STATUS_TUTOR_MOVES.length)])); @@ -389,6 +390,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde specialOptions.push(rareFormChangeModifier); } if (specialOptions.length > 0) { + // TODO: should this use `randSeedItem`? modifierOptions.push(specialOptions[randSeedInt(specialOptions.length)]); } diff --git a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts index 553c4deb74e..90a0188692e 100644 --- a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts +++ b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts @@ -139,6 +139,7 @@ export const ClowningAroundEncounter: MysteryEncounter = MysteryEncounterBuilder clownConfig.partyTemplateFunc = null; // Overrides party template func if it exists // Generate random ability for Blacephalon from pool + // TODO: should this use `randSeedItem`? const ability = RANDOM_ABILITY_POOL[randSeedInt(RANDOM_ABILITY_POOL.length)]; encounter.setDialogueToken("ability", allAbilities[ability].name); encounter.misc = { ability }; diff --git a/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts b/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts index 9c9232612c6..4a99fc69ae2 100644 --- a/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts +++ b/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts @@ -190,6 +190,7 @@ async function doBiomeTransitionDialogueAndBattleInit() { // Calculate new biome (cannot be current biome) const filteredBiomes = BIOME_CANDIDATES.filter(b => globalScene.arena.biomeType !== b); + // TODO: should this use `randSeedItem`? const newBiome = filteredBiomes[randSeedInt(filteredBiomes.length)]; // Show dialogue and transition biome diff --git a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts index 1ba756c7f5d..3316c844287 100644 --- a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts +++ b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts @@ -314,6 +314,7 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit // One random pokemon will get its passive unlocked const passiveDisabledPokemon = globalScene.getPlayerParty().filter(p => !p.passive); if (passiveDisabledPokemon?.length > 0) { + // TODO: should this use `randSeedItem`? const enablePassiveMon = passiveDisabledPokemon[randSeedInt(passiveDisabledPokemon.length)]; enablePassiveMon.passive = true; enablePassiveMon.updateInfo(true); @@ -479,6 +480,7 @@ async function doNewTeamPostProcess(transformations: PokemonTransformation[]) { // One random pokemon will get its passive unlocked const passiveDisabledPokemon = globalScene.getPlayerParty().filter(p => !p.passive); if (passiveDisabledPokemon?.length > 0) { + // TODO: should this use `randSeedItem`? const enablePassiveMon = passiveDisabledPokemon[randSeedInt(passiveDisabledPokemon.length)]; enablePassiveMon.passive = true; await enablePassiveMon.updateInfo(true); diff --git a/src/data/mystery-encounters/mystery-encounter-option.ts b/src/data/mystery-encounters/mystery-encounter-option.ts index 53b53392bb8..999d448e305 100644 --- a/src/data/mystery-encounters/mystery-encounter-option.ts +++ b/src/data/mystery-encounters/mystery-encounter-option.ts @@ -145,11 +145,13 @@ export default class MysteryEncounterOption implements IMysteryEncounterOption { } if (truePrimaryPool.length > 0) { // always choose from the non-overlapping pokemon first + // TODO: should this use `randSeedItem`? this.primaryPokemon = truePrimaryPool[randSeedInt(truePrimaryPool.length)]; return true; } // if there are multiple overlapping pokemon, we're okay - just choose one and take it out of the supporting pokemon pool if (overlap.length > 1 || this.secondaryPokemon.length - overlap.length >= 1) { + // TODO: should this use `randSeedItem`? this.primaryPokemon = overlap[randSeedInt(overlap.length)]; this.secondaryPokemon = this.secondaryPokemon.filter(supp => supp !== this.primaryPokemon); return true; diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts index fa97a7f4d40..bc0f56b4925 100644 --- a/src/data/mystery-encounters/mystery-encounter.ts +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -385,6 +385,7 @@ export default class MysteryEncounter implements IMysteryEncounter { // If there are multiple overlapping pokemon, we're okay - just choose one and take it out of the primary pokemon pool if (overlap.length > 1 || this.secondaryPokemon.length - overlap.length >= 1) { // is this working? + // TODO: should this use `randSeedItem`? this.primaryPokemon = overlap[randSeedInt(overlap.length, 0)]; this.secondaryPokemon = this.secondaryPokemon.filter(supp => supp !== this.primaryPokemon); return true; @@ -395,6 +396,7 @@ export default class MysteryEncounter implements IMysteryEncounter { return false; } // this means we CAN have the same pokemon be a primary and secondary pokemon, so just choose any qualifying one randomly. + // TODO: should this use `randSeedItem`? this.primaryPokemon = qualified[randSeedInt(qualified.length, 0)]; return true; } diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index bd2dfa998f4..70e3e34a333 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -1099,8 +1099,10 @@ export function calculateMEAggregateStats(baseSpawnWeight: number) { if (biomes! && biomes.length > 0) { const specialBiomes = biomes.filter(b => alwaysPickTheseBiomes.includes(b)); if (specialBiomes.length > 0) { + // TODO: should this use `randSeedItem`? currentBiome = specialBiomes[randSeedInt(specialBiomes.length)]; } else { + // TODO: should this use `randSeedItem`? currentBiome = biomes[randSeedInt(biomes.length)]; } } diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index f3655217b5a..18df10a369a 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -111,20 +111,24 @@ export function getRandomPlayerPokemon( // If there is only 1 legal/unfainted mon left, select from fainted legal mons const faintedLegalMons = party.filter(p => (!isAllowed || p.isAllowedInChallenge()) && p.isFainted()); if (faintedLegalMons.length > 0) { + // TODO: should this use `randSeedItem`? chosenIndex = randSeedInt(faintedLegalMons.length); chosenPokemon = faintedLegalMons[chosenIndex]; } } if (!chosenPokemon && fullyLegalMons.length > 0) { + // TODO: should this use `randSeedItem`? chosenIndex = randSeedInt(fullyLegalMons.length); chosenPokemon = fullyLegalMons[chosenIndex]; } if (!chosenPokemon && isAllowed && allowedOnlyMons.length > 0) { + // TODO: should this use `randSeedItem`? chosenIndex = randSeedInt(allowedOnlyMons.length); chosenPokemon = allowedOnlyMons[chosenIndex]; } if (!chosenPokemon) { // If no other options worked, returns fully random + // TODO: should this use `randSeedItem`? chosenIndex = randSeedInt(party.length); chosenPokemon = party[chosenIndex]; } diff --git a/src/field/arena.ts b/src/field/arena.ts index 4479748667c..1f4e48afa17 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -146,6 +146,7 @@ export class Arena { if (!tierPool.length) { ret = globalScene.randomSpecies(waveIndex, level); } else { + // TODO: should this use `randSeedItem`? const entry = tierPool[randSeedInt(tierPool.length)]; let species: SpeciesId; if (typeof entry === "number") { @@ -157,6 +158,7 @@ export class Arena { if (level >= levelThreshold) { const speciesIds = entry[levelThreshold]; if (speciesIds.length > 1) { + // TODO: should this use `randSeedItem`? species = speciesIds[randSeedInt(speciesIds.length)]; } else { species = speciesIds[0]; diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 0a8e8469115..3eb382e567d 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/field/trainer.ts b/src/field/trainer.ts index 8ac896e2717..a4d125eb9da 100644 --- a/src/field/trainer.ts +++ b/src/field/trainer.ts @@ -421,7 +421,8 @@ export default class Trainer extends Phaser.GameObjects.Container { // If useNewSpeciesPool is true, we need to generate a new species from the new species pool, otherwise we generate a random species let species = useNewSpeciesPool - ? getPokemonSpecies(newSpeciesPool[Math.floor(randSeedInt(newSpeciesPool.length))]) + ? // TODO: should this use `randSeedItem`? + getPokemonSpecies(newSpeciesPool[Math.floor(randSeedInt(newSpeciesPool.length))]) : template.isSameSpecies(index) && index > offset ? getPokemonSpecies( battle.enemyParty[offset].species.getTrainerSpeciesForLevel( @@ -619,6 +620,8 @@ export default class Trainer extends Phaser.GameObjects.Container { if (maxScorePartyMemberIndexes.length > 1) { let rand: number; + // TODO: should this use `randSeedItem`? + globalScene.executeWithSeedOffset( () => (rand = randSeedInt(maxScorePartyMemberIndexes.length)), globalScene.currentBattle.turn << 2, 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..eff5e084ec7 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, randSeedItem } 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"; @@ -1565,6 +1558,7 @@ class TmModifierTypeGenerator extends ModifierTypeGenerator { if (!tierUniqueCompatibleTms.length) { return null; } + // TODO: should this use `randSeedItem`? const randTmIndex = randSeedInt(tierUniqueCompatibleTms.length); return new TmModifierType(tierUniqueCompatibleTms[randTmIndex]); }); @@ -1618,6 +1612,7 @@ class EvolutionItemModifierTypeGenerator extends ModifierTypeGenerator { return null; } + // TODO: should this use `randSeedItem`? return new EvolutionItemModifierType(evolutionItemPool[randSeedInt(evolutionItemPool.length)]!); // TODO: is the bang correct? }); } @@ -1703,6 +1698,7 @@ export class FormChangeItemModifierTypeGenerator extends ModifierTypeGenerator { return null; } + // TODO: should this use `randSeedItem`? return new FormChangeItemModifierType(formChangeItemPool[randSeedInt(formChangeItemPool.length)]); }); } @@ -1973,7 +1969,7 @@ const modifierTypeInitObj = Object.freeze({ if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in Nature) { return new PokemonNatureChangeModifierType(pregenArgs[0] as Nature); } - return new PokemonNatureChangeModifierType(randSeedInt(getEnumValues(Nature).length) as Nature); + return new PokemonNatureChangeModifierType(randSeedItem(getEnumValues(Nature))); }), MYSTICAL_ROCK: () => diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 3bc46d3aea8..5f301fabe01 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/phases/select-biome-phase.ts b/src/phases/select-biome-phase.ts index e8b4946b6d1..679d159bc01 100644 --- a/src/phases/select-biome-phase.ts +++ b/src/phases/select-biome-phase.ts @@ -56,6 +56,7 @@ export class SelectBiomePhase extends BattlePhase { delay: 1000, }); } else { + // TODO: should this use `randSeedItem`? setNextBiome(biomes[randSeedInt(biomes.length)]); } } else if (biomeLinks.hasOwnProperty(currentBiome)) { 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 753d6ebb865..e3a3f89a970 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -8,11 +8,19 @@ export type nil = null | undefined; export const MissingTextureKey = "__MISSING"; +// TODO: Draft tests for these utility functions +// TODO: Break up this file +/** + * 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(" "); } @@ -273,18 +281,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)); } @@ -636,25 +632,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..c13b5dc780e --- /dev/null +++ b/src/utils/enums.ts @@ -0,0 +1,74 @@ +import type { EnumOrObject, EnumValues, TSNumericEnum, NormalEnum } from "#app/@types/enum-types"; +import type { InferKeys } from "#app/@types/type-helpers"; + +/** + * 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 + * @example + * enum fruit { + * apple = 1, + * banana = 2, + * cherry = 3, + * orange = 12, + * }; + * + * console.log(getEnumKeys(fruit)); // output: ["apple", "banana", "cherry", "orange"] + * @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 numbers or reverse mapped strings, so we can retrieve the keys by filtering out numbers. + 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 + * @example + * enum fruit { + * apple = 1, + * banana = 2, + * cherry = 3, + * orange = 12, + * }; + * + * console.log(getEnumValues(fruit)); // output: [1, 2, 3, 12] + * + * @remarks + * To retrieve the keys of a {@linkcode NormalEnum}, use {@linkcode Object.values} instead. + */ +// NB: This intentionally does not use `EnumValues` as using `E[keyof E]` leads to improved 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 NormalEnum} 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, 2)); // 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, + * but the return type will be the union of ALL their corresponding keys. + */ +export function enumValueToKey>( + object: NormalEnum, + val: V, +): InferKeys { + for (const [key, value] of Object.entries(object)) { + if (val === value) { + return key as InferKeys; + } + } + throw new Error(`Invalid value passed to \`enumValueToKey\`! Value: ${val}`); +} diff --git a/test/types/enum-types.test-d.ts b/test/types/enum-types.test-d.ts new file mode 100644 index 00000000000..bdc7146d41a --- /dev/null +++ b/test/types/enum-types.test-d.ts @@ -0,0 +1,107 @@ +import type { EnumOrObject, EnumValues, TSNumericEnum, NormalEnum } from "#app/@types/enum-types"; + +import type { getEnumKeys, getEnumValues } from "#app/utils/enums"; +import type { enumValueToKey } from "#app/utils/enums"; + +import { expectTypeOf, describe, it } from "vitest"; + +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; + +describe("Enum Type Helpers", () => { + describe("EnumValues", () => { + it("should go from enum object type to value type", () => { + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().branded.toEqualTypeOf<1 | 2>(); + + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toMatchTypeOf<"apple" | "banana">(); + }); + + it("should produce union of const object values as type", () => { + expectTypeOf>().toEqualTypeOf<1 | 2>(); + + expectTypeOf>().toEqualTypeOf<"apple" | "banana">(); + }); + }); + + describe("TSNumericEnum", () => { + it("should match numeric enums", () => { + expectTypeOf>().toEqualTypeOf(); + }); + + it("should not match string enums or const objects", () => { + expectTypeOf>().toBeNever(); + expectTypeOf>().toBeNever(); + expectTypeOf>().toBeNever(); + }); + }); + + describe("NormalEnum", () => { + it("should match string enums or const objects", () => { + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + }); + it("should not match numeric enums", () => { + expectTypeOf>().toBeNever(); + }); + }); + + describe("EnumOrObject", () => { + it("should match any enum or const object", () => { + expectTypeOf().toMatchTypeOf(); + expectTypeOf().toMatchTypeOf(); + expectTypeOf().toMatchTypeOf(); + expectTypeOf().toMatchTypeOf(); + }); + + it("should not match an enum value union w/o typeof", () => { + expectTypeOf().not.toMatchTypeOf(); + expectTypeOf().not.toMatchTypeOf(); + }); + + it("should be equivalent to `TSNumericEnum | NormalEnum`", () => { + expectTypeOf().branded.toEqualTypeOf | NormalEnum>(); + }); + }); +}); + +describe("Enum Functions", () => { + describe("getEnumKeys", () => { + it("should retrieve keys of numeric enum", () => { + expectTypeOf>().returns.toEqualTypeOf<("testN1" | "testN2")[]>(); + }); + }); + + describe("getEnumValues", () => { + it("should retrieve values of numeric enum", () => { + expectTypeOf>().returns.branded.toEqualTypeOf<(1 | 2)[]>(); + }); + }); + + describe("enumValueToKey", () => { + it("should retrieve values for a given key", () => { + expectTypeOf< + typeof enumValueToKey + >().returns.toEqualTypeOf<"testS1">(); + expectTypeOf>().returns.toEqualTypeOf< + "testS1" | "testS2" + >(); + expectTypeOf>().returns.toEqualTypeOf<"testON1">(); + expectTypeOf>().returns.toEqualTypeOf<"testON1" | "testON2">(); + }); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts index c781bde97ed..14c8f0508eb 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -28,19 +28,23 @@ export default defineProject(({ mode }) => ({ } }, }, - environment: "jsdom" as const, + environment: "jsdom", environmentOptions: { jsdom: { resources: "usable", }, }, + typecheck: { + tsconfig: "tsconfig.json", + include: ["./test/types/**/*.{test,spec}{-|.}d.ts"], + }, threads: false, trace: true, restoreMocks: true, watch: false, coverage: { - provider: "istanbul" as const, - reportsDirectory: "coverage" as const, + provider: "istanbul", + reportsDirectory: "coverage", reporters: ["text-summary", "html"], }, name: "main",