mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-07 07:59:26 +02:00
Merge branch 'beta' into move-discard-button
This commit is contained in:
commit
cc8312b24a
@ -19,7 +19,6 @@
|
|||||||
// and having to verify whether each individual file is ignored
|
// and having to verify whether each individual file is ignored
|
||||||
"includes": [
|
"includes": [
|
||||||
"**",
|
"**",
|
||||||
"!**/*.d.ts",
|
|
||||||
"!**/dist/**/*",
|
"!**/dist/**/*",
|
||||||
"!**/build/**/*",
|
"!**/build/**/*",
|
||||||
"!**/coverage/**/*",
|
"!**/coverage/**/*",
|
||||||
@ -180,7 +179,7 @@
|
|||||||
// Overrides to prevent unused import removal inside `overrides.ts` and enums files (for TSDoc linkcodes),
|
// Overrides to prevent unused import removal inside `overrides.ts` and enums files (for TSDoc linkcodes),
|
||||||
// as well as in all TS files in `scripts/` (which are assumed to be boilerplate templates).
|
// as well as in all TS files in `scripts/` (which are assumed to be boilerplate templates).
|
||||||
{
|
{
|
||||||
"includes": ["**/src/overrides.ts", "**/src/enums/**/*", "**/scripts/**/*.ts"],
|
"includes": ["**/src/overrides.ts", "**/src/enums/**/*", "**/scripts/**/*.ts", "**/*.d.ts"],
|
||||||
"linter": {
|
"linter": {
|
||||||
"rules": {
|
"rules": {
|
||||||
"correctness": {
|
"correctness": {
|
||||||
|
10
global.d.ts
vendored
10
global.d.ts
vendored
@ -1,7 +1,6 @@
|
|||||||
|
import type { AnyFn } from "#types/type-helpers";
|
||||||
import type { SetupServerApi } from "msw/node";
|
import type { SetupServerApi } from "msw/node";
|
||||||
|
|
||||||
export {};
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
/**
|
/**
|
||||||
* Only used in testing.
|
* Only used in testing.
|
||||||
@ -11,4 +10,11 @@ declare global {
|
|||||||
* To set up your own server in a test see `game-data.test.ts`
|
* To set up your own server in a test see `game-data.test.ts`
|
||||||
*/
|
*/
|
||||||
var server: SetupServerApi;
|
var server: SetupServerApi;
|
||||||
|
|
||||||
|
// Overloads for `Function.apply` and `Function.call` to add type safety on matching argument types
|
||||||
|
interface Function {
|
||||||
|
apply<T extends AnyFn>(this: T, thisArg: ThisParameterType<T>, argArray: Parameters<T>): ReturnType<T>;
|
||||||
|
|
||||||
|
call<T extends AnyFn>(this: T, thisArg: ThisParameterType<T>, ...argArray: Parameters<T>): ReturnType<T>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -94,3 +94,12 @@ export type AbstractConstructor<T> = abstract new (...args: any[]) => T;
|
|||||||
export type CoerceNullPropertiesToUndefined<T extends object> = {
|
export type CoerceNullPropertiesToUndefined<T extends object> = {
|
||||||
[K in keyof T]: null extends T[K] ? Exclude<T[K], null> | undefined : T[K];
|
[K in keyof T]: null extends T[K] ? Exclude<T[K], null> | undefined : T[K];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type helper to mark all properties in `T` as optional, while still mandating that at least 1
|
||||||
|
* of its properties be present.
|
||||||
|
*
|
||||||
|
* Distinct from {@linkcode Partial} as this requires at least 1 property to _not_ be undefined.
|
||||||
|
* @typeParam T - The type to render partial
|
||||||
|
*/
|
||||||
|
export type AtLeastOne<T extends object> = Partial<T> & ObjectValues<{ [K in keyof T]: Pick<Required<T>, K> }>;
|
||||||
|
@ -3528,6 +3528,7 @@ export class BattleScene extends SceneBase {
|
|||||||
this.gameMode.hasMysteryEncounters &&
|
this.gameMode.hasMysteryEncounters &&
|
||||||
battleType === BattleType.WILD &&
|
battleType === BattleType.WILD &&
|
||||||
!this.gameMode.isBoss(waveIndex) &&
|
!this.gameMode.isBoss(waveIndex) &&
|
||||||
|
waveIndex % 10 !== 1 &&
|
||||||
waveIndex < highestMysteryEncounterWave &&
|
waveIndex < highestMysteryEncounterWave &&
|
||||||
waveIndex > lowestMysteryEncounterWave
|
waveIndex > lowestMysteryEncounterWave
|
||||||
);
|
);
|
||||||
|
@ -1191,11 +1191,11 @@ export const pokemonEvolutions: PokemonEvolutions = {
|
|||||||
new SpeciesEvolution(SpeciesId.KOMMO_O, 45, null, null)
|
new SpeciesEvolution(SpeciesId.KOMMO_O, 45, null, null)
|
||||||
],
|
],
|
||||||
[SpeciesId.COSMOG]: [
|
[SpeciesId.COSMOG]: [
|
||||||
new SpeciesEvolution(SpeciesId.COSMOEM, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 40}, SpeciesWildEvolutionDelay.VERY_LONG)
|
new SpeciesEvolution(SpeciesId.COSMOEM, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 43}, SpeciesWildEvolutionDelay.VERY_LONG)
|
||||||
],
|
],
|
||||||
[SpeciesId.COSMOEM]: [
|
[SpeciesId.COSMOEM]: [
|
||||||
new SpeciesEvolution(SpeciesId.SOLGALEO, 23, EvolutionItem.SUN_FLUTE, null, SpeciesWildEvolutionDelay.VERY_LONG),
|
new SpeciesEvolution(SpeciesId.SOLGALEO, 13, EvolutionItem.SUN_FLUTE, null, SpeciesWildEvolutionDelay.VERY_LONG),
|
||||||
new SpeciesEvolution(SpeciesId.LUNALA, 23, EvolutionItem.MOON_FLUTE, null, SpeciesWildEvolutionDelay.VERY_LONG)
|
new SpeciesEvolution(SpeciesId.LUNALA, 13, EvolutionItem.MOON_FLUTE, null, SpeciesWildEvolutionDelay.VERY_LONG)
|
||||||
],
|
],
|
||||||
[SpeciesId.MELTAN]: [
|
[SpeciesId.MELTAN]: [
|
||||||
new SpeciesEvolution(SpeciesId.MELMETAL, 48, null, null)
|
new SpeciesEvolution(SpeciesId.MELMETAL, 48, null, null)
|
||||||
|
1784
src/data/balance/pokemon-species.ts
Normal file
1784
src/data/balance/pokemon-species.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -3560,6 +3560,7 @@ export class GrudgeTag extends SerializableBattlerTag {
|
|||||||
* @param sourcePokemon - The source of the move that fainted the tag's bearer
|
* @param sourcePokemon - The source of the move that fainted the tag's bearer
|
||||||
* @returns `false` if Grudge activates its effect or lapses
|
* @returns `false` if Grudge activates its effect or lapses
|
||||||
*/
|
*/
|
||||||
|
// TODO: Confirm whether this should interact with copying moves
|
||||||
override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType, sourcePokemon?: Pokemon): boolean {
|
override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType, sourcePokemon?: Pokemon): boolean {
|
||||||
if (lapseType === BattlerTagLapseType.CUSTOM && sourcePokemon) {
|
if (lapseType === BattlerTagLapseType.CUSTOM && sourcePokemon) {
|
||||||
if (sourcePokemon.isActive() && pokemon.isOpponent(sourcePokemon)) {
|
if (sourcePokemon.isActive() && pokemon.isOpponent(sourcePokemon)) {
|
||||||
|
@ -6,7 +6,6 @@ import { pokemonEvolutions } from "#balance/pokemon-evolutions";
|
|||||||
import { speciesStarterCosts } from "#balance/starters";
|
import { speciesStarterCosts } from "#balance/starters";
|
||||||
import { pokemonFormChanges } from "#data/pokemon-forms";
|
import { pokemonFormChanges } from "#data/pokemon-forms";
|
||||||
import type { PokemonSpecies } from "#data/pokemon-species";
|
import type { PokemonSpecies } from "#data/pokemon-species";
|
||||||
import { getPokemonSpeciesForm } from "#data/pokemon-species";
|
|
||||||
import { BattleType } from "#enums/battle-type";
|
import { BattleType } from "#enums/battle-type";
|
||||||
import { ChallengeType } from "#enums/challenge-type";
|
import { ChallengeType } from "#enums/challenge-type";
|
||||||
import { Challenges } from "#enums/challenges";
|
import { Challenges } from "#enums/challenges";
|
||||||
@ -26,7 +25,7 @@ import { PokemonMove } from "#moves/pokemon-move";
|
|||||||
import type { DexAttrProps, GameData } from "#system/game-data";
|
import type { DexAttrProps, GameData } from "#system/game-data";
|
||||||
import { BooleanHolder, isBetween, type NumberHolder, randSeedItem } from "#utils/common";
|
import { BooleanHolder, isBetween, type NumberHolder, randSeedItem } from "#utils/common";
|
||||||
import { deepCopy } from "#utils/data";
|
import { deepCopy } from "#utils/data";
|
||||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils";
|
||||||
import { toCamelCase, toSnakeCase } from "#utils/strings";
|
import { toCamelCase, toSnakeCase } from "#utils/strings";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { pokerogueApi } from "#api/pokerogue-api";
|
|||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
import { speciesStarterCosts } from "#balance/starters";
|
import { speciesStarterCosts } from "#balance/starters";
|
||||||
import type { PokemonSpeciesForm } from "#data/pokemon-species";
|
import type { PokemonSpeciesForm } from "#data/pokemon-species";
|
||||||
import { getPokemonSpeciesForm, PokemonSpecies } from "#data/pokemon-species";
|
import { PokemonSpecies } from "#data/pokemon-species";
|
||||||
import { BiomeId } from "#enums/biome-id";
|
import { BiomeId } from "#enums/biome-id";
|
||||||
import { PartyMemberStrength } from "#enums/party-member-strength";
|
import { PartyMemberStrength } from "#enums/party-member-strength";
|
||||||
import type { SpeciesId } from "#enums/species-id";
|
import type { SpeciesId } from "#enums/species-id";
|
||||||
@ -10,7 +10,7 @@ import { PlayerPokemon } from "#field/pokemon";
|
|||||||
import type { Starter } from "#ui/starter-select-ui-handler";
|
import type { Starter } from "#ui/starter-select-ui-handler";
|
||||||
import { randSeedGauss, randSeedInt, randSeedItem } from "#utils/common";
|
import { randSeedGauss, randSeedInt, randSeedItem } from "#utils/common";
|
||||||
import { getEnumValues } from "#utils/enums";
|
import { getEnumValues } from "#utils/enums";
|
||||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils";
|
||||||
|
|
||||||
export interface DailyRunConfig {
|
export interface DailyRunConfig {
|
||||||
seed: number;
|
seed: number;
|
||||||
|
@ -59,7 +59,7 @@ export const TeleportingHijinksEncounter: MysteryEncounter = MysteryEncounterBui
|
|||||||
)
|
)
|
||||||
.withEncounterTier(MysteryEncounterTier.COMMON)
|
.withEncounterTier(MysteryEncounterTier.COMMON)
|
||||||
.withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES)
|
.withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES)
|
||||||
.withSceneRequirement(new WaveModulusRequirement([1, 2, 3], 10)) // Must be in first 3 waves after boss wave
|
.withSceneRequirement(new WaveModulusRequirement([2, 3, 4], 10)) // Must be in first 3 waves after boss wave
|
||||||
.withSceneRequirement(new MoneyRequirement(0, MONEY_COST_MULTIPLIER)) // Must be able to pay teleport cost
|
.withSceneRequirement(new MoneyRequirement(0, MONEY_COST_MULTIPLIER)) // Must be able to pay teleport cost
|
||||||
.withAutoHideIntroVisuals(false)
|
.withAutoHideIntroVisuals(false)
|
||||||
.withCatchAllowed(true)
|
.withCatchAllowed(true)
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -3,7 +3,7 @@ import { loadBattlerTag, SerializableBattlerTag } from "#data/battler-tags";
|
|||||||
import { allSpecies } from "#data/data-lists";
|
import { allSpecies } from "#data/data-lists";
|
||||||
import type { Gender } from "#data/gender";
|
import type { Gender } from "#data/gender";
|
||||||
import { PokemonMove } from "#data/moves/pokemon-move";
|
import { PokemonMove } from "#data/moves/pokemon-move";
|
||||||
import { getPokemonSpeciesForm, type PokemonSpeciesForm } from "#data/pokemon-species";
|
import type { PokemonSpeciesForm } from "#data/pokemon-species";
|
||||||
import type { TypeDamageMultiplier } from "#data/type";
|
import type { TypeDamageMultiplier } from "#data/type";
|
||||||
import type { AbilityId } from "#enums/ability-id";
|
import type { AbilityId } from "#enums/ability-id";
|
||||||
import type { BerryType } from "#enums/berry-type";
|
import type { BerryType } from "#enums/berry-type";
|
||||||
@ -16,6 +16,7 @@ import type { IllusionData } from "#types/illusion-data";
|
|||||||
import type { TurnMove } from "#types/turn-move";
|
import type { TurnMove } from "#types/turn-move";
|
||||||
import type { CoerceNullPropertiesToUndefined } from "#types/type-helpers";
|
import type { CoerceNullPropertiesToUndefined } from "#types/type-helpers";
|
||||||
import { isNullOrUndefined } from "#utils/common";
|
import { isNullOrUndefined } from "#utils/common";
|
||||||
|
import { getPokemonSpeciesForm } from "#utils/pokemon-utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type that {@linkcode PokemonSpeciesForm} is converted to when an object containing it serializes it.
|
* The type that {@linkcode PokemonSpeciesForm} is converted to when an object containing it serializes it.
|
||||||
|
@ -60,7 +60,7 @@ import {
|
|||||||
} from "#data/pokemon-data";
|
} from "#data/pokemon-data";
|
||||||
import type { SpeciesFormChange } from "#data/pokemon-forms";
|
import type { SpeciesFormChange } from "#data/pokemon-forms";
|
||||||
import type { PokemonSpeciesForm } from "#data/pokemon-species";
|
import type { PokemonSpeciesForm } from "#data/pokemon-species";
|
||||||
import { getFusedSpeciesName, getPokemonSpeciesForm, PokemonSpecies } from "#data/pokemon-species";
|
import { PokemonSpecies } from "#data/pokemon-species";
|
||||||
import { getRandomStatus, getStatusEffectOverlapText, Status } from "#data/status-effect";
|
import { getRandomStatus, getStatusEffectOverlapText, Status } from "#data/status-effect";
|
||||||
import { getTerrainBlockMessage, TerrainType } from "#data/terrain";
|
import { getTerrainBlockMessage, TerrainType } from "#data/terrain";
|
||||||
import type { TypeDamageMultiplier } from "#data/type";
|
import type { TypeDamageMultiplier } from "#data/type";
|
||||||
@ -168,7 +168,7 @@ import {
|
|||||||
toDmgValue,
|
toDmgValue,
|
||||||
} from "#utils/common";
|
} from "#utils/common";
|
||||||
import { getEnumValues } from "#utils/enums";
|
import { getEnumValues } from "#utils/enums";
|
||||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
import { getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils";
|
||||||
import { argbFromRgba, QuantizerCelebi, rgbaFromArgb } from "@material/material-color-utilities";
|
import { argbFromRgba, QuantizerCelebi, rgbaFromArgb } from "@material/material-color-utilities";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import Phaser from "phaser";
|
import Phaser from "phaser";
|
||||||
|
@ -2,10 +2,10 @@ import { initAbilities } from "#abilities/ability";
|
|||||||
import { initBiomes } from "#balance/biomes";
|
import { initBiomes } from "#balance/biomes";
|
||||||
import { initEggMoves } from "#balance/egg-moves";
|
import { initEggMoves } from "#balance/egg-moves";
|
||||||
import { initPokemonPrevolutions, initPokemonStarters } from "#balance/pokemon-evolutions";
|
import { initPokemonPrevolutions, initPokemonStarters } from "#balance/pokemon-evolutions";
|
||||||
|
import { initSpecies } from "#balance/pokemon-species";
|
||||||
import { initChallenges } from "#data/challenge";
|
import { initChallenges } from "#data/challenge";
|
||||||
import { initTrainerTypeDialogue } from "#data/dialogue";
|
import { initTrainerTypeDialogue } from "#data/dialogue";
|
||||||
import { initPokemonForms } from "#data/pokemon-forms";
|
import { initPokemonForms } from "#data/pokemon-forms";
|
||||||
import { initSpecies } from "#data/pokemon-species";
|
|
||||||
import { initModifierPools } from "#modifiers/init-modifier-pools";
|
import { initModifierPools } from "#modifiers/init-modifier-pools";
|
||||||
import { initModifierTypes } from "#modifiers/modifier-type";
|
import { initModifierTypes } from "#modifiers/modifier-type";
|
||||||
import { initMoves } from "#moves/move";
|
import { initMoves } from "#moves/move";
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
import type { Gender } from "#data/gender";
|
import type { Gender } from "#data/gender";
|
||||||
import { CustomPokemonData, PokemonBattleData, PokemonSummonData } from "#data/pokemon-data";
|
import { CustomPokemonData, PokemonBattleData, PokemonSummonData } from "#data/pokemon-data";
|
||||||
import { getPokemonSpeciesForm } from "#data/pokemon-species";
|
|
||||||
import { Status } from "#data/status-effect";
|
import { Status } from "#data/status-effect";
|
||||||
import { BattleType } from "#enums/battle-type";
|
import { BattleType } from "#enums/battle-type";
|
||||||
import type { BiomeId } from "#enums/biome-id";
|
import type { BiomeId } from "#enums/biome-id";
|
||||||
@ -14,7 +13,7 @@ import { TrainerSlot } from "#enums/trainer-slot";
|
|||||||
import { EnemyPokemon, Pokemon } from "#field/pokemon";
|
import { EnemyPokemon, Pokemon } from "#field/pokemon";
|
||||||
import { PokemonMove } from "#moves/pokemon-move";
|
import { PokemonMove } from "#moves/pokemon-move";
|
||||||
import type { Variant } from "#sprites/variant";
|
import type { Variant } from "#sprites/variant";
|
||||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils";
|
||||||
|
|
||||||
export class PokemonData {
|
export class PokemonData {
|
||||||
public id: number;
|
public id: number;
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
import { getPokemonSpeciesForm } from "#data/pokemon-species";
|
|
||||||
import { DexAttr } from "#enums/dex-attr";
|
import { DexAttr } from "#enums/dex-attr";
|
||||||
import type { SessionSaveData, SystemSaveData } from "#system/game-data";
|
import type { SessionSaveData, SystemSaveData } from "#system/game-data";
|
||||||
import type { SessionSaveMigrator } from "#types/session-save-migrator";
|
import type { SessionSaveMigrator } from "#types/session-save-migrator";
|
||||||
import type { SystemSaveMigrator } from "#types/system-save-migrator";
|
import type { SystemSaveMigrator } from "#types/system-save-migrator";
|
||||||
import { isNullOrUndefined } from "#utils/common";
|
import { isNullOrUndefined } from "#utils/common";
|
||||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If a starter is caught, but the only forms registered as caught are not starterSelectable,
|
* If a starter is caught, but the only forms registered as caught are not starterSelectable,
|
||||||
|
1
src/typings/i18next.d.ts
vendored
1
src/typings/i18next.d.ts
vendored
@ -3,6 +3,7 @@ import type { TOptions } from "i18next";
|
|||||||
// Module declared to make referencing keys in the localization files type-safe.
|
// Module declared to make referencing keys in the localization files type-safe.
|
||||||
declare module "i18next" {
|
declare module "i18next" {
|
||||||
interface TFunction {
|
interface TFunction {
|
||||||
|
// biome-ignore lint/style/useShorthandFunctionType: This needs to be an interface due to interface merging
|
||||||
(key: string | string[], options?: TOptions & Record<string, unknown>): string;
|
(key: string | string[], options?: TOptions & Record<string, unknown>): string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ import { getNatureName } from "#data/nature";
|
|||||||
import type { SpeciesFormChange } from "#data/pokemon-forms";
|
import type { SpeciesFormChange } from "#data/pokemon-forms";
|
||||||
import { pokemonFormChanges } from "#data/pokemon-forms";
|
import { pokemonFormChanges } from "#data/pokemon-forms";
|
||||||
import type { PokemonSpecies } from "#data/pokemon-species";
|
import type { PokemonSpecies } from "#data/pokemon-species";
|
||||||
import { getPokemonSpeciesForm, normalForm } from "#data/pokemon-species";
|
import { normalForm } from "#data/pokemon-species";
|
||||||
import { AbilityAttr } from "#enums/ability-attr";
|
import { AbilityAttr } from "#enums/ability-attr";
|
||||||
import type { AbilityId } from "#enums/ability-id";
|
import type { AbilityId } from "#enums/ability-id";
|
||||||
import { BiomeId } from "#enums/biome-id";
|
import { BiomeId } from "#enums/biome-id";
|
||||||
@ -56,7 +56,7 @@ import { addBBCodeTextObject, addTextObject, getTextColor, getTextStyleOptions }
|
|||||||
import { addWindow } from "#ui/ui-theme";
|
import { addWindow } from "#ui/ui-theme";
|
||||||
import { BooleanHolder, getLocalizedSpriteKey, isNullOrUndefined, padInt, rgbHexToRgba } from "#utils/common";
|
import { BooleanHolder, getLocalizedSpriteKey, isNullOrUndefined, padInt, rgbHexToRgba } from "#utils/common";
|
||||||
import { getEnumValues } from "#utils/enums";
|
import { getEnumValues } from "#utils/enums";
|
||||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils";
|
||||||
import { toTitleCase } from "#utils/strings";
|
import { toTitleCase } from "#utils/strings";
|
||||||
import { argbFromRgba } from "@material/material-color-utilities";
|
import { argbFromRgba } from "@material/material-color-utilities";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
@ -15,7 +15,7 @@ import {
|
|||||||
import { speciesTmMoves } from "#balance/tms";
|
import { speciesTmMoves } from "#balance/tms";
|
||||||
import { allAbilities, allMoves, allSpecies } from "#data/data-lists";
|
import { allAbilities, allMoves, allSpecies } from "#data/data-lists";
|
||||||
import type { PokemonForm, PokemonSpecies } from "#data/pokemon-species";
|
import type { PokemonForm, PokemonSpecies } from "#data/pokemon-species";
|
||||||
import { getPokemonSpeciesForm, getPokerusStarters, normalForm } from "#data/pokemon-species";
|
import { normalForm } from "#data/pokemon-species";
|
||||||
import { AbilityAttr } from "#enums/ability-attr";
|
import { AbilityAttr } from "#enums/ability-attr";
|
||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
import { BiomeId } from "#enums/biome-id";
|
import { BiomeId } from "#enums/biome-id";
|
||||||
@ -46,6 +46,7 @@ import { addWindow } from "#ui/ui-theme";
|
|||||||
import { BooleanHolder, fixedInt, getLocalizedSpriteKey, padInt, randIntRange, rgbHexToRgba } from "#utils/common";
|
import { BooleanHolder, fixedInt, getLocalizedSpriteKey, padInt, randIntRange, rgbHexToRgba } from "#utils/common";
|
||||||
import type { StarterPreferences } from "#utils/data";
|
import type { StarterPreferences } from "#utils/data";
|
||||||
import { loadStarterPreferences } from "#utils/data";
|
import { loadStarterPreferences } from "#utils/data";
|
||||||
|
import { getPokemonSpeciesForm, getPokerusStarters } from "#utils/pokemon-utils";
|
||||||
import { argbFromRgba } from "@material/material-color-utilities";
|
import { argbFromRgba } from "@material/material-color-utilities";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ import { allMoves } from "#data/data-lists";
|
|||||||
import { getEggTierForSpecies } from "#data/egg";
|
import { getEggTierForSpecies } from "#data/egg";
|
||||||
import type { EggHatchData } from "#data/egg-hatch-data";
|
import type { EggHatchData } from "#data/egg-hatch-data";
|
||||||
import { Gender } from "#data/gender";
|
import { Gender } from "#data/gender";
|
||||||
import { getPokemonSpeciesForm } from "#data/pokemon-species";
|
|
||||||
import { PokemonType } from "#enums/pokemon-type";
|
import { PokemonType } from "#enums/pokemon-type";
|
||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
import { TextStyle } from "#enums/text-style";
|
import { TextStyle } from "#enums/text-style";
|
||||||
@ -13,6 +12,7 @@ import type { PlayerPokemon } from "#field/pokemon";
|
|||||||
import { PokemonInfoContainer } from "#ui/pokemon-info-container";
|
import { PokemonInfoContainer } from "#ui/pokemon-info-container";
|
||||||
import { addTextObject } from "#ui/text";
|
import { addTextObject } from "#ui/text";
|
||||||
import { padInt, rgbHexToRgba } from "#utils/common";
|
import { padInt, rgbHexToRgba } from "#utils/common";
|
||||||
|
import { getPokemonSpeciesForm } from "#utils/pokemon-utils";
|
||||||
import { argbFromRgba } from "@material/material-color-utilities";
|
import { argbFromRgba } from "@material/material-color-utilities";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -24,7 +24,6 @@ import { Gender, getGenderColor, getGenderSymbol } from "#data/gender";
|
|||||||
import { getNatureName } from "#data/nature";
|
import { getNatureName } from "#data/nature";
|
||||||
import { pokemonFormChanges } from "#data/pokemon-forms";
|
import { pokemonFormChanges } from "#data/pokemon-forms";
|
||||||
import type { PokemonSpecies } from "#data/pokemon-species";
|
import type { PokemonSpecies } from "#data/pokemon-species";
|
||||||
import { getPokemonSpeciesForm, getPokerusStarters } from "#data/pokemon-species";
|
|
||||||
import { AbilityAttr } from "#enums/ability-attr";
|
import { AbilityAttr } from "#enums/ability-attr";
|
||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
import { Button } from "#enums/buttons";
|
import { Button } from "#enums/buttons";
|
||||||
@ -72,6 +71,7 @@ import {
|
|||||||
} from "#utils/common";
|
} from "#utils/common";
|
||||||
import type { StarterPreferences } from "#utils/data";
|
import type { StarterPreferences } from "#utils/data";
|
||||||
import { loadStarterPreferences, saveStarterPreferences } from "#utils/data";
|
import { loadStarterPreferences, saveStarterPreferences } from "#utils/data";
|
||||||
|
import { getPokemonSpeciesForm, getPokerusStarters } from "#utils/pokemon-utils";
|
||||||
import { toTitleCase } from "#utils/strings";
|
import { toTitleCase } from "#utils/strings";
|
||||||
import { argbFromRgba } from "@material/material-color-utilities";
|
import { argbFromRgba } from "@material/material-color-utilities";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
|
import { globalScene } from "#app/global-scene";
|
||||||
|
import { POKERUS_STARTER_COUNT, speciesStarterCosts } from "#balance/starters";
|
||||||
import { allSpecies } from "#data/data-lists";
|
import { allSpecies } from "#data/data-lists";
|
||||||
import type { PokemonSpecies } from "#data/pokemon-species";
|
import type { PokemonSpecies, PokemonSpeciesForm } from "#data/pokemon-species";
|
||||||
import type { SpeciesId } from "#enums/species-id";
|
import type { SpeciesId } from "#enums/species-id";
|
||||||
|
import { randSeedItem } from "./common";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the {@linkcode PokemonSpecies} object associated with the {@linkcode SpeciesId} enum given
|
* Gets the {@linkcode PokemonSpecies} object associated with the {@linkcode SpeciesId} enum given
|
||||||
@ -19,3 +22,104 @@ export function getPokemonSpecies(species: SpeciesId | SpeciesId[]): PokemonSpec
|
|||||||
}
|
}
|
||||||
return allSpecies[species - 1];
|
return allSpecies[species - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to get the daily list of starters with Pokerus.
|
||||||
|
* @returns A list of starters with Pokerus
|
||||||
|
*/
|
||||||
|
export function getPokerusStarters(): PokemonSpecies[] {
|
||||||
|
const pokerusStarters: PokemonSpecies[] = [];
|
||||||
|
const date = new Date();
|
||||||
|
date.setUTCHours(0, 0, 0, 0);
|
||||||
|
globalScene.executeWithSeedOffset(
|
||||||
|
() => {
|
||||||
|
while (pokerusStarters.length < POKERUS_STARTER_COUNT) {
|
||||||
|
const randomSpeciesId = Number.parseInt(randSeedItem(Object.keys(speciesStarterCosts)), 10);
|
||||||
|
const species = getPokemonSpecies(randomSpeciesId);
|
||||||
|
if (!pokerusStarters.includes(species)) {
|
||||||
|
pokerusStarters.push(species);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
date.getTime().toString(),
|
||||||
|
);
|
||||||
|
return pokerusStarters;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFusedSpeciesName(speciesAName: string, speciesBName: string): string {
|
||||||
|
const fragAPattern = /([a-z]{2}.*?[aeiou(?:y$)\-']+)(.*?)$/i;
|
||||||
|
const fragBPattern = /([a-z]{2}.*?[aeiou(?:y$)\-'])(.*?)$/i;
|
||||||
|
|
||||||
|
const [speciesAPrefixMatch, speciesBPrefixMatch] = [speciesAName, speciesBName].map(n => /^(?:[^ ]+) /.exec(n));
|
||||||
|
const [speciesAPrefix, speciesBPrefix] = [speciesAPrefixMatch, speciesBPrefixMatch].map(m => (m ? m[0] : ""));
|
||||||
|
|
||||||
|
if (speciesAPrefix) {
|
||||||
|
speciesAName = speciesAName.slice(speciesAPrefix.length);
|
||||||
|
}
|
||||||
|
if (speciesBPrefix) {
|
||||||
|
speciesBName = speciesBName.slice(speciesBPrefix.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [speciesASuffixMatch, speciesBSuffixMatch] = [speciesAName, speciesBName].map(n => / (?:[^ ]+)$/.exec(n));
|
||||||
|
const [speciesASuffix, speciesBSuffix] = [speciesASuffixMatch, speciesBSuffixMatch].map(m => (m ? m[0] : ""));
|
||||||
|
|
||||||
|
if (speciesASuffix) {
|
||||||
|
speciesAName = speciesAName.slice(0, -speciesASuffix.length);
|
||||||
|
}
|
||||||
|
if (speciesBSuffix) {
|
||||||
|
speciesBName = speciesBName.slice(0, -speciesBSuffix.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
const splitNameA = speciesAName.split(/ /g);
|
||||||
|
const splitNameB = speciesBName.split(/ /g);
|
||||||
|
|
||||||
|
const fragAMatch = fragAPattern.exec(speciesAName);
|
||||||
|
const fragBMatch = fragBPattern.exec(speciesBName);
|
||||||
|
|
||||||
|
let fragA: string;
|
||||||
|
let fragB: string;
|
||||||
|
|
||||||
|
fragA = splitNameA.length === 1 ? (fragAMatch ? fragAMatch[1] : speciesAName) : splitNameA[splitNameA.length - 1];
|
||||||
|
|
||||||
|
if (splitNameB.length === 1) {
|
||||||
|
if (fragBMatch) {
|
||||||
|
const lastCharA = fragA.slice(fragA.length - 1);
|
||||||
|
const prevCharB = fragBMatch[1].slice(fragBMatch.length - 1);
|
||||||
|
fragB = (/[-']/.test(prevCharB) ? prevCharB : "") + fragBMatch[2] || prevCharB;
|
||||||
|
if (lastCharA === fragB[0]) {
|
||||||
|
if (/[aiu]/.test(lastCharA)) {
|
||||||
|
fragB = fragB.slice(1);
|
||||||
|
} else {
|
||||||
|
const newCharMatch = new RegExp(`[^${lastCharA}]`).exec(fragB);
|
||||||
|
if (newCharMatch?.index !== undefined && newCharMatch.index > 0) {
|
||||||
|
fragB = fragB.slice(newCharMatch.index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fragB = speciesBName;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fragB = splitNameB[splitNameB.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (splitNameA.length > 1) {
|
||||||
|
fragA = `${splitNameA.slice(0, splitNameA.length - 1).join(" ")} ${fragA}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
fragB = `${fragB.slice(0, 1).toLowerCase()}${fragB.slice(1)}`;
|
||||||
|
|
||||||
|
return `${speciesAPrefix || speciesBPrefix}${fragA}${fragB}${speciesBSuffix || speciesASuffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPokemonSpeciesForm(species: SpeciesId, formIndex: number): PokemonSpeciesForm {
|
||||||
|
const retSpecies: PokemonSpecies =
|
||||||
|
species >= 2000
|
||||||
|
? allSpecies.find(s => s.speciesId === species)! // TODO: is the bang correct?
|
||||||
|
: allSpecies[species - 1];
|
||||||
|
if (formIndex < retSpecies.forms?.length) {
|
||||||
|
return retSpecies.forms[formIndex];
|
||||||
|
}
|
||||||
|
return retSpecies;
|
||||||
|
}
|
||||||
|
2
src/vite.env.d.ts
vendored
2
src/vite.env.d.ts
vendored
@ -12,5 +12,5 @@ interface ImportMetaEnv {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
readonly env: ImportMetaEnv
|
readonly env: ImportMetaEnv;
|
||||||
}
|
}
|
||||||
|
133
test/@types/vitest.d.ts
vendored
133
test/@types/vitest.d.ts
vendored
@ -1,26 +1,139 @@
|
|||||||
import type { Pokemon } from "#field/pokemon";
|
import type { TerrainType } from "#app/data/terrain";
|
||||||
|
import type { AbilityId } from "#enums/ability-id";
|
||||||
|
import type { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
|
import type { MoveId } from "#enums/move-id";
|
||||||
import type { PokemonType } from "#enums/pokemon-type";
|
import type { PokemonType } from "#enums/pokemon-type";
|
||||||
import type { expect } from "vitest";
|
import type { BattleStat, EffectiveStat, Stat } from "#enums/stat";
|
||||||
|
import type { StatusEffect } from "#enums/status-effect";
|
||||||
|
import type { WeatherType } from "#enums/weather-type";
|
||||||
|
import type { Pokemon } from "#field/pokemon";
|
||||||
|
import type { ToHaveEffectiveStatMatcherOptions } from "#test/test-utils/matchers/to-have-effective-stat";
|
||||||
|
import type { expectedStatusType } from "#test/test-utils/matchers/to-have-status-effect";
|
||||||
import type { toHaveTypesOptions } from "#test/test-utils/matchers/to-have-types";
|
import type { toHaveTypesOptions } from "#test/test-utils/matchers/to-have-types";
|
||||||
|
import type { TurnMove } from "#types/turn-move";
|
||||||
|
import type { AtLeastOne } from "#types/type-helpers";
|
||||||
|
import type { expect } from "vitest";
|
||||||
|
import type Overrides from "#app/overrides";
|
||||||
|
import type { PokemonMove } from "#moves/pokemon-move";
|
||||||
|
|
||||||
declare module "vitest" {
|
declare module "vitest" {
|
||||||
interface Assertion {
|
interface Assertion {
|
||||||
/**
|
/**
|
||||||
* Matcher to check if an array contains EXACTLY the given items (in any order).
|
* Check whether an array contains EXACTLY the given items (in any order).
|
||||||
*
|
*
|
||||||
* Different from {@linkcode expect.arrayContaining} as the latter only requires the array contain
|
* Different from {@linkcode expect.arrayContaining} as the latter only checks for subset equality
|
||||||
* _at least_ the listed items.
|
* (as opposed to full equality).
|
||||||
*
|
*
|
||||||
* @param expected - The expected contents of the array, in any order.
|
* @param expected - The expected contents of the array, in any order
|
||||||
* @see {@linkcode expect.arrayContaining}
|
* @see {@linkcode expect.arrayContaining}
|
||||||
*/
|
*/
|
||||||
toEqualArrayUnsorted<E>(expected: E[]): void;
|
toEqualArrayUnsorted<E>(expected: E[]): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Matcher to check if a {@linkcode Pokemon}'s current typing includes the given types.
|
* Check whether a {@linkcode Pokemon}'s current typing includes the given types.
|
||||||
*
|
*
|
||||||
* @param expected - The expected types (in any order).
|
* @param expected - The expected types (in any order)
|
||||||
* @param options - The options passed to the matcher.
|
* @param options - The options passed to the matcher
|
||||||
*/
|
*/
|
||||||
toHaveTypes(expected: PokemonType[], options?: toHaveTypesOptions): void;
|
toHaveTypes(expected: [PokemonType, ...PokemonType[]], options?: toHaveTypesOptions): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matcher to check the contents of a {@linkcode Pokemon}'s move history.
|
||||||
|
*
|
||||||
|
* @param expectedValue - The expected value; can be a {@linkcode MoveId} or a partially filled {@linkcode TurnMove}
|
||||||
|
* containing the desired properties to check
|
||||||
|
* @param index - The index of the move history entry to check, in order from most recent to least recent.
|
||||||
|
* Default `0` (last used move)
|
||||||
|
* @see {@linkcode Pokemon.getLastXMoves}
|
||||||
|
*/
|
||||||
|
toHaveUsedMove(expected: MoveId | AtLeastOne<TurnMove>, index?: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a {@linkcode Pokemon}'s effective stat is as expected
|
||||||
|
* (checked after all stat value modifications).
|
||||||
|
*
|
||||||
|
* @param stat - The {@linkcode EffectiveStat} to check
|
||||||
|
* @param expectedValue - The expected value of {@linkcode stat}
|
||||||
|
* @param options - (Optional) The {@linkcode ToHaveEffectiveStatMatcherOptions}
|
||||||
|
* @remarks
|
||||||
|
* If you want to check the stat **before** modifiers are applied, use {@linkcode Pokemon.getStat} instead.
|
||||||
|
*/
|
||||||
|
toHaveEffectiveStat(stat: EffectiveStat, expectedValue: number, options?: ToHaveEffectiveStatMatcherOptions): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a {@linkcode Pokemon} has taken a specific amount of damage.
|
||||||
|
* @param expectedDamageTaken - The expected amount of damage taken
|
||||||
|
* @param roundDown - Whether to round down {@linkcode expectedDamageTaken} with {@linkcode toDmgValue}; default `true`
|
||||||
|
*/
|
||||||
|
toHaveTakenDamage(expectedDamageTaken: number, roundDown?: boolean): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the current {@linkcode WeatherType} is as expected.
|
||||||
|
* @param expectedWeatherType - The expected {@linkcode WeatherType}
|
||||||
|
*/
|
||||||
|
toHaveWeather(expectedWeatherType: WeatherType): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the current {@linkcode TerrainType} is as expected.
|
||||||
|
* @param expectedTerrainType - The expected {@linkcode TerrainType}
|
||||||
|
*/
|
||||||
|
toHaveTerrain(expectedTerrainType: TerrainType): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a {@linkcode Pokemon} is at full HP.
|
||||||
|
*/
|
||||||
|
toHaveFullHp(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a {@linkcode Pokemon} has a specific {@linkcode StatusEffect | non-volatile status effect}.
|
||||||
|
* @param expectedStatusEffect - The {@linkcode StatusEffect} the Pokemon is expected to have,
|
||||||
|
* or a partially filled {@linkcode Status} containing the desired properties
|
||||||
|
*/
|
||||||
|
toHaveStatusEffect(expectedStatusEffect: expectedStatusType): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a {@linkcode Pokemon} has a specific {@linkcode Stat} stage.
|
||||||
|
* @param stat - The {@linkcode BattleStat} to check
|
||||||
|
* @param expectedStage - The expected stat stage value of {@linkcode stat}
|
||||||
|
*/
|
||||||
|
toHaveStatStage(stat: BattleStat, expectedStage: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a {@linkcode Pokemon} has a specific {@linkcode BattlerTagType}.
|
||||||
|
* @param expectedBattlerTagType - The expected {@linkcode BattlerTagType}
|
||||||
|
*/
|
||||||
|
toHaveBattlerTag(expectedBattlerTagType: BattlerTagType): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a {@linkcode Pokemon} has applied a specific {@linkcode AbilityId}.
|
||||||
|
* @param expectedAbilityId - The expected {@linkcode AbilityId}
|
||||||
|
*/
|
||||||
|
toHaveAbilityApplied(expectedAbilityId: AbilityId): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a {@linkcode Pokemon} has a specific amount of {@linkcode Stat.HP | HP}.
|
||||||
|
* @param expectedHp - The expected amount of {@linkcode Stat.HP | HP} to have
|
||||||
|
*/
|
||||||
|
toHaveHp(expectedHp: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a {@linkcode Pokemon} is currently fainted (as determined by {@linkcode Pokemon.isFainted}).
|
||||||
|
* @remarks
|
||||||
|
* When checking whether an enemy wild Pokemon is fainted, one must reference it in a variable _before_ the fainting effect occurs
|
||||||
|
* as otherwise the Pokemon will be GC'ed and rendered `undefined`.
|
||||||
|
*/
|
||||||
|
toHaveFainted(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a {@linkcode Pokemon} has consumed the given amount of PP for one of its moves.
|
||||||
|
* @param expectedValue - The {@linkcode MoveId} of the {@linkcode PokemonMove} that should have consumed PP
|
||||||
|
* @param ppUsed - The numerical amount of PP that should have been consumed,
|
||||||
|
* or `all` to indicate the move should be _out_ of PP
|
||||||
|
* @remarks
|
||||||
|
* If the Pokemon's moveset has been set via {@linkcode Overrides.MOVESET_OVERRIDE}/{@linkcode Overrides.OPP_MOVESET_OVERRIDE},
|
||||||
|
* does not contain {@linkcode expectedMove}
|
||||||
|
* or contains the desired move more than once, this will fail the test.
|
||||||
|
*/
|
||||||
|
toHaveUsedPP(expectedMove: MoveId, ppUsed: number | "all"): void;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,5 +1,18 @@
|
|||||||
import { toEqualArrayUnsorted } from "#test/test-utils/matchers/to-equal-array-unsorted";
|
import { toEqualArrayUnsorted } from "#test/test-utils/matchers/to-equal-array-unsorted";
|
||||||
|
import { toHaveAbilityApplied } from "#test/test-utils/matchers/to-have-ability-applied";
|
||||||
|
import { toHaveBattlerTag } from "#test/test-utils/matchers/to-have-battler-tag";
|
||||||
|
import { toHaveEffectiveStat } from "#test/test-utils/matchers/to-have-effective-stat";
|
||||||
|
import { toHaveFainted } from "#test/test-utils/matchers/to-have-fainted";
|
||||||
|
import { toHaveFullHp } from "#test/test-utils/matchers/to-have-full-hp";
|
||||||
|
import { toHaveHp } from "#test/test-utils/matchers/to-have-hp";
|
||||||
|
import { toHaveStatStage } from "#test/test-utils/matchers/to-have-stat-stage";
|
||||||
|
import { toHaveStatusEffect } from "#test/test-utils/matchers/to-have-status-effect";
|
||||||
|
import { toHaveTakenDamage } from "#test/test-utils/matchers/to-have-taken-damage";
|
||||||
|
import { toHaveTerrain } from "#test/test-utils/matchers/to-have-terrain";
|
||||||
import { toHaveTypes } from "#test/test-utils/matchers/to-have-types";
|
import { toHaveTypes } from "#test/test-utils/matchers/to-have-types";
|
||||||
|
import { toHaveUsedMove } from "#test/test-utils/matchers/to-have-used-move";
|
||||||
|
import { toHaveUsedPP } from "#test/test-utils/matchers/to-have-used-pp";
|
||||||
|
import { toHaveWeather } from "#test/test-utils/matchers/to-have-weather";
|
||||||
import { expect } from "vitest";
|
import { expect } from "vitest";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -10,4 +23,17 @@ import { expect } from "vitest";
|
|||||||
expect.extend({
|
expect.extend({
|
||||||
toEqualArrayUnsorted,
|
toEqualArrayUnsorted,
|
||||||
toHaveTypes,
|
toHaveTypes,
|
||||||
|
toHaveUsedMove,
|
||||||
|
toHaveEffectiveStat,
|
||||||
|
toHaveTakenDamage,
|
||||||
|
toHaveWeather,
|
||||||
|
toHaveTerrain,
|
||||||
|
toHaveFullHp,
|
||||||
|
toHaveStatusEffect,
|
||||||
|
toHaveStatStage,
|
||||||
|
toHaveBattlerTag,
|
||||||
|
toHaveAbilityApplied,
|
||||||
|
toHaveHp,
|
||||||
|
toHaveFainted,
|
||||||
|
toHaveUsedPP,
|
||||||
});
|
});
|
||||||
|
@ -2,6 +2,7 @@ import { AbilityId } from "#enums/ability-id";
|
|||||||
import { BattlerIndex } from "#enums/battler-index";
|
import { BattlerIndex } from "#enums/battler-index";
|
||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
|
import { WeatherType } from "#enums/weather-type";
|
||||||
import { GameManager } from "#test/test-utils/game-manager";
|
import { GameManager } from "#test/test-utils/game-manager";
|
||||||
import Phaser from "phaser";
|
import Phaser from "phaser";
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||||
@ -23,68 +24,64 @@ describe("Moves - Grudge", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
game = new GameManager(phaserGame);
|
game = new GameManager(phaserGame);
|
||||||
game.override
|
game.override
|
||||||
.moveset([MoveId.EMBER, MoveId.SPLASH])
|
|
||||||
.ability(AbilityId.BALL_FETCH)
|
.ability(AbilityId.BALL_FETCH)
|
||||||
.battleStyle("single")
|
.battleStyle("single")
|
||||||
.criticalHits(false)
|
.criticalHits(false)
|
||||||
.enemySpecies(SpeciesId.SHEDINJA)
|
.enemySpecies(SpeciesId.RATTATA)
|
||||||
.enemyAbility(AbilityId.WONDER_GUARD)
|
.startingLevel(100)
|
||||||
.enemyMoveset([MoveId.GRUDGE, MoveId.SPLASH]);
|
.enemyAbility(AbilityId.NO_GUARD);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should reduce the PP of the Pokemon's move to 0 when the user has fainted", async () => {
|
it("should reduce the PP of an attack that faints the user to 0", async () => {
|
||||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||||
|
|
||||||
const playerPokemon = game.scene.getPlayerPokemon();
|
const feebas = game.field.getPlayerPokemon();
|
||||||
game.move.select(MoveId.EMBER);
|
const ratatta = game.field.getEnemyPokemon();
|
||||||
await game.move.selectEnemyMove(MoveId.GRUDGE);
|
|
||||||
|
game.move.use(MoveId.GUILLOTINE);
|
||||||
|
await game.move.forceEnemyMove(MoveId.GRUDGE);
|
||||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
await game.phaseInterceptor.to("FaintPhase");
|
||||||
|
|
||||||
const playerMove = playerPokemon?.getMoveset().find(m => m.moveId === MoveId.EMBER);
|
// Ratatta should have fainted and consumed all of Guillotine's PP
|
||||||
|
expect(ratatta).toHaveFainted();
|
||||||
expect(playerMove?.getPpRatio()).toBe(0);
|
expect(feebas).toHaveUsedPP(MoveId.GUILLOTINE, "all");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should remain in effect until the user's next move", async () => {
|
it("should remain in effect until the user's next move", async () => {
|
||||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||||
|
|
||||||
const playerPokemon = game.scene.getPlayerPokemon();
|
const feebas = game.field.getPlayerPokemon();
|
||||||
game.move.select(MoveId.SPLASH);
|
const ratatta = game.field.getEnemyPokemon();
|
||||||
await game.move.selectEnemyMove(MoveId.GRUDGE);
|
|
||||||
|
game.move.use(MoveId.SPLASH);
|
||||||
|
await game.move.forceEnemyMove(MoveId.GRUDGE);
|
||||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
|
|
||||||
game.move.select(MoveId.EMBER);
|
game.move.use(MoveId.GUILLOTINE);
|
||||||
await game.move.selectEnemyMove(MoveId.SPLASH);
|
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
const playerMove = playerPokemon?.getMoveset().find(m => m.moveId === MoveId.EMBER);
|
expect(ratatta).toHaveFainted();
|
||||||
|
expect(feebas).toHaveUsedPP(MoveId.GUILLOTINE, "all");
|
||||||
expect(playerMove?.getPpRatio()).toBe(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not reduce the opponent's PP if the user dies to weather/indirect damage", async () => {
|
it("should not reduce PP if the user dies to weather/indirect damage", async () => {
|
||||||
// Opponent will be reduced to 1 HP by False Swipe, then faint to Sandstorm
|
// Opponent will be reduced to 1 HP by False Swipe, then faint to Sandstorm
|
||||||
game.override
|
game.override.weather(WeatherType.SANDSTORM);
|
||||||
.moveset([MoveId.FALSE_SWIPE])
|
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||||
.startingLevel(100)
|
|
||||||
.ability(AbilityId.SAND_STREAM)
|
|
||||||
.enemySpecies(SpeciesId.RATTATA);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.GEODUDE]);
|
|
||||||
|
|
||||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
const feebas = game.field.getPlayerPokemon();
|
||||||
const playerPokemon = game.scene.getPlayerPokemon();
|
const ratatta = game.field.getEnemyPokemon();
|
||||||
|
|
||||||
game.move.select(MoveId.FALSE_SWIPE);
|
game.move.use(MoveId.FALSE_SWIPE);
|
||||||
await game.move.selectEnemyMove(MoveId.GRUDGE);
|
await game.move.forceEnemyMove(MoveId.GRUDGE);
|
||||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
expect(enemyPokemon?.isFainted()).toBe(true);
|
expect(ratatta).toHaveFainted();
|
||||||
|
expect(feebas).toHaveUsedPP(MoveId.FALSE_SWIPE, 1);
|
||||||
const playerMove = playerPokemon?.getMoveset().find(m => m.moveId === MoveId.FALSE_SWIPE);
|
|
||||||
expect(playerMove?.getPpRatio()).toBeGreaterThan(0);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
126
test/moves/spite.test.ts
Normal file
126
test/moves/spite.test.ts
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import { AbilityId } from "#enums/ability-id";
|
||||||
|
import { BattlerIndex } from "#enums/battler-index";
|
||||||
|
import { MoveId } from "#enums/move-id";
|
||||||
|
import { MoveResult } from "#enums/move-result";
|
||||||
|
import { MoveUseMode } from "#enums/move-use-mode";
|
||||||
|
import { SpeciesId } from "#enums/species-id";
|
||||||
|
import { GameManager } from "#test/test-utils/game-manager";
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
describe("Moves - Spite", () => {
|
||||||
|
let phaserGame: Phaser.Game;
|
||||||
|
let game: GameManager;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
game.override
|
||||||
|
.ability(AbilityId.BALL_FETCH)
|
||||||
|
.battleStyle("single")
|
||||||
|
.criticalHits(false)
|
||||||
|
.enemySpecies(SpeciesId.MAGIKARP)
|
||||||
|
.enemyAbility(AbilityId.BALL_FETCH)
|
||||||
|
.startingLevel(100)
|
||||||
|
.enemyLevel(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reduce the PP of the target's last used move by 4", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||||
|
|
||||||
|
const karp = game.field.getEnemyPokemon();
|
||||||
|
game.move.changeMoveset(karp, [MoveId.SPLASH, MoveId.TACKLE]);
|
||||||
|
|
||||||
|
game.move.use(MoveId.SPITE);
|
||||||
|
await game.move.selectEnemyMove(MoveId.TACKLE);
|
||||||
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(karp).toHaveUsedPP(MoveId.TACKLE, 1);
|
||||||
|
|
||||||
|
game.move.use(MoveId.SPITE);
|
||||||
|
await game.move.selectEnemyMove(MoveId.SPLASH);
|
||||||
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(karp).toHaveUsedPP(MoveId.TACKLE, 4 + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail if the target has not used a move", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||||
|
|
||||||
|
const karp = game.field.getEnemyPokemon();
|
||||||
|
game.move.changeMoveset(karp, [MoveId.SPLASH, MoveId.TACKLE]);
|
||||||
|
|
||||||
|
game.move.use(MoveId.SPITE);
|
||||||
|
await game.move.selectEnemyMove(MoveId.TACKLE);
|
||||||
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
const feebas = game.field.getPlayerPokemon();
|
||||||
|
expect(feebas).toHaveUsedMove({ move: MoveId.SPITE, result: MoveResult.FAIL });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail if the target's last used move is out of PP", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||||
|
|
||||||
|
const karp = game.field.getEnemyPokemon();
|
||||||
|
game.move.changeMoveset(karp, [MoveId.TACKLE]);
|
||||||
|
karp.moveset[0].ppUsed = 0;
|
||||||
|
|
||||||
|
game.move.use(MoveId.SPITE);
|
||||||
|
await game.move.selectEnemyMove(MoveId.TACKLE);
|
||||||
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
const feebas = game.field.getPlayerPokemon();
|
||||||
|
expect(feebas).toHaveUsedMove({ move: MoveId.SPITE, result: MoveResult.FAIL });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail if the target's last used move is not in their moveset", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||||
|
|
||||||
|
const karp = game.field.getEnemyPokemon();
|
||||||
|
game.move.changeMoveset(karp, [MoveId.TACKLE]);
|
||||||
|
// Fake magikarp having used Splash the turn prior
|
||||||
|
karp.pushMoveHistory({ move: MoveId.SPLASH, targets: [BattlerIndex.ENEMY], useMode: MoveUseMode.NORMAL });
|
||||||
|
|
||||||
|
game.move.use(MoveId.SPITE);
|
||||||
|
await game.move.selectEnemyMove(MoveId.TACKLE);
|
||||||
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
const feebas = game.field.getPlayerPokemon();
|
||||||
|
expect(feebas).toHaveUsedMove({ move: MoveId.SPITE, result: MoveResult.FAIL });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should ignore virtual and Dancer-induced moves", async () => {
|
||||||
|
game.override.battleStyle("double").enemyAbility(AbilityId.DANCER);
|
||||||
|
game.move.forceMetronomeMove(MoveId.SPLASH);
|
||||||
|
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||||
|
|
||||||
|
const [karp1, karp2] = game.scene.getEnemyField();
|
||||||
|
game.move.changeMoveset(karp1, [MoveId.SPLASH, MoveId.METRONOME, MoveId.SWORDS_DANCE]);
|
||||||
|
game.move.changeMoveset(karp2, [MoveId.SWORDS_DANCE, MoveId.TACKLE]);
|
||||||
|
|
||||||
|
game.move.use(MoveId.SPITE);
|
||||||
|
await game.move.selectEnemyMove(MoveId.METRONOME);
|
||||||
|
await game.move.selectEnemyMove(MoveId.SWORDS_DANCE);
|
||||||
|
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER]);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
// Spite ignored virtual splash and swords dance, instead only docking from metronome
|
||||||
|
expect(karp1).toHaveUsedPP(MoveId.SPLASH, 0);
|
||||||
|
expect(karp1).toHaveUsedPP(MoveId.SWORDS_DANCE, 0);
|
||||||
|
expect(karp1).toHaveUsedPP(MoveId.METRONOME, 5);
|
||||||
|
});
|
||||||
|
});
|
@ -174,7 +174,7 @@ describe("Berries Abound - Mystery Encounter", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should start battle if fastest pokemon is slower than boss below wave 50", async () => {
|
it("should start battle if fastest pokemon is slower than boss below wave 50", async () => {
|
||||||
game.override.startingWave(41);
|
game.override.startingWave(42);
|
||||||
const encounterTextSpy = vi.spyOn(EncounterDialogueUtils, "showEncounterText");
|
const encounterTextSpy = vi.spyOn(EncounterDialogueUtils, "showEncounterText");
|
||||||
await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty);
|
await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty);
|
||||||
|
|
||||||
|
@ -253,7 +253,7 @@ describe("Bug-Type Superfan - Mystery Encounter", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should start battle against the Bug-Type Superfan with wave 70 party template", async () => {
|
it("should start battle against the Bug-Type Superfan with wave 70 party template", async () => {
|
||||||
game.override.startingWave(61);
|
game.override.startingWave(63);
|
||||||
await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty);
|
await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty);
|
||||||
await runMysteryEncounterToEnd(game, 1, undefined, true);
|
await runMysteryEncounterToEnd(game, 1, undefined, true);
|
||||||
|
|
||||||
@ -268,7 +268,7 @@ describe("Bug-Type Superfan - Mystery Encounter", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should start battle against the Bug-Type Superfan with wave 100 party template", async () => {
|
it("should start battle against the Bug-Type Superfan with wave 100 party template", async () => {
|
||||||
game.override.startingWave(81);
|
game.override.startingWave(83);
|
||||||
await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty);
|
await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty);
|
||||||
await runMysteryEncounterToEnd(game, 1, undefined, true);
|
await runMysteryEncounterToEnd(game, 1, undefined, true);
|
||||||
|
|
||||||
@ -284,7 +284,7 @@ describe("Bug-Type Superfan - Mystery Encounter", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should start battle against the Bug-Type Superfan with wave 120 party template", async () => {
|
it("should start battle against the Bug-Type Superfan with wave 120 party template", async () => {
|
||||||
game.override.startingWave(111);
|
game.override.startingWave(113);
|
||||||
await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty);
|
await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty);
|
||||||
await runMysteryEncounterToEnd(game, 1, undefined, true);
|
await runMysteryEncounterToEnd(game, 1, undefined, true);
|
||||||
|
|
||||||
@ -302,7 +302,7 @@ describe("Bug-Type Superfan - Mystery Encounter", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should start battle against the Bug-Type Superfan with wave 140 party template", async () => {
|
it("should start battle against the Bug-Type Superfan with wave 140 party template", async () => {
|
||||||
game.override.startingWave(131);
|
game.override.startingWave(133);
|
||||||
await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty);
|
await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty);
|
||||||
await runMysteryEncounterToEnd(game, 1, undefined, true);
|
await runMysteryEncounterToEnd(game, 1, undefined, true);
|
||||||
|
|
||||||
@ -320,7 +320,7 @@ describe("Bug-Type Superfan - Mystery Encounter", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should start battle against the Bug-Type Superfan with wave 160 party template", async () => {
|
it("should start battle against the Bug-Type Superfan with wave 160 party template", async () => {
|
||||||
game.override.startingWave(151);
|
game.override.startingWave(153);
|
||||||
await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty);
|
await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty);
|
||||||
await runMysteryEncounterToEnd(game, 1, undefined, true);
|
await runMysteryEncounterToEnd(game, 1, undefined, true);
|
||||||
|
|
||||||
@ -338,7 +338,7 @@ describe("Bug-Type Superfan - Mystery Encounter", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should start battle against the Bug-Type Superfan with wave 180 party template", async () => {
|
it("should start battle against the Bug-Type Superfan with wave 180 party template", async () => {
|
||||||
game.override.startingWave(171);
|
game.override.startingWave(173);
|
||||||
await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty);
|
await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty);
|
||||||
await runMysteryEncounterToEnd(game, 1, undefined, true);
|
await runMysteryEncounterToEnd(game, 1, undefined, true);
|
||||||
|
|
||||||
|
@ -79,14 +79,6 @@ describe("Teleporting Hijinks - Mystery Encounter", () => {
|
|||||||
expect(TeleportingHijinksEncounter.options.length).toBe(3);
|
expect(TeleportingHijinksEncounter.options.length).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should run in waves that are X1", async () => {
|
|
||||||
game.override.startingWave(11).mysteryEncounterTier(MysteryEncounterTier.COMMON);
|
|
||||||
|
|
||||||
await game.runToMysteryEncounter();
|
|
||||||
|
|
||||||
expect(scene.currentBattle?.mysteryEncounter?.encounterType).toBe(MysteryEncounterType.TELEPORTING_HIJINKS);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should run in waves that are X2", async () => {
|
it("should run in waves that are X2", async () => {
|
||||||
game.override.startingWave(32).mysteryEncounterTier(MysteryEncounterTier.COMMON);
|
game.override.startingWave(32).mysteryEncounterTier(MysteryEncounterTier.COMMON);
|
||||||
|
|
||||||
@ -103,8 +95,16 @@ describe("Teleporting Hijinks - Mystery Encounter", () => {
|
|||||||
expect(scene.currentBattle?.mysteryEncounter?.encounterType).toBe(MysteryEncounterType.TELEPORTING_HIJINKS);
|
expect(scene.currentBattle?.mysteryEncounter?.encounterType).toBe(MysteryEncounterType.TELEPORTING_HIJINKS);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should NOT run in waves that are not X1, X2, or X3", async () => {
|
it("should run in waves that are X4", async () => {
|
||||||
game.override.startingWave(54);
|
game.override.startingWave(54).mysteryEncounterTier(MysteryEncounterTier.COMMON);
|
||||||
|
|
||||||
|
await game.runToMysteryEncounter();
|
||||||
|
|
||||||
|
expect(scene.currentBattle?.mysteryEncounter?.encounterType).toBe(MysteryEncounterType.TELEPORTING_HIJINKS);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should NOT run in waves that are not X2, X3, or X4", async () => {
|
||||||
|
game.override.startingWave(67);
|
||||||
|
|
||||||
await game.runToMysteryEncounter();
|
await game.runToMysteryEncounter();
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ describe("Mystery Encounters", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
game = new GameManager(phaserGame);
|
game = new GameManager(phaserGame);
|
||||||
scene = game.scene;
|
scene = game.scene;
|
||||||
game.override.startingWave(11).mysteryEncounterChance(100);
|
game.override.startingWave(12).mysteryEncounterChance(100);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Spawns a mystery encounter", async () => {
|
it("Spawns a mystery encounter", async () => {
|
||||||
@ -37,12 +37,20 @@ describe("Mystery Encounters", () => {
|
|||||||
expect(game.scene.phaseManager.getCurrentPhase()!.constructor.name).toBe(MysteryEncounterPhase.name);
|
expect(game.scene.phaseManager.getCurrentPhase()!.constructor.name).toBe(MysteryEncounterPhase.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Encounters should not run on X1 waves", async () => {
|
||||||
|
game.override.startingWave(11);
|
||||||
|
|
||||||
|
await game.runToMysteryEncounter();
|
||||||
|
|
||||||
|
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it("Encounters should not run below wave 10", async () => {
|
it("Encounters should not run below wave 10", async () => {
|
||||||
game.override.startingWave(9);
|
game.override.startingWave(9);
|
||||||
|
|
||||||
await game.runToMysteryEncounter();
|
await game.runToMysteryEncounter();
|
||||||
|
|
||||||
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.MYSTERIOUS_CHALLENGERS);
|
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Encounters should not run above wave 180", async () => {
|
it("Encounters should not run above wave 180", async () => {
|
||||||
|
@ -27,7 +27,7 @@ describe("Mystery Encounter Phases", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
game = new GameManager(phaserGame);
|
game = new GameManager(phaserGame);
|
||||||
game.override.startingWave(11).mysteryEncounterChance(100).seed("test"); // Seed guarantees wild encounter to be replaced by ME
|
game.override.startingWave(12).mysteryEncounterChance(100).seed("test"); // Seed guarantees wild encounter to be replaced by ME
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("MysteryEncounterPhase", () => {
|
describe("MysteryEncounterPhase", () => {
|
||||||
|
@ -3,7 +3,6 @@ import type { BattleScene } from "#app/battle-scene";
|
|||||||
import { getGameMode } from "#app/game-mode";
|
import { getGameMode } from "#app/game-mode";
|
||||||
import { getDailyRunStarters } from "#data/daily-run";
|
import { getDailyRunStarters } from "#data/daily-run";
|
||||||
import { Gender } from "#data/gender";
|
import { Gender } from "#data/gender";
|
||||||
import { getPokemonSpeciesForm } from "#data/pokemon-species";
|
|
||||||
import { BattleType } from "#enums/battle-type";
|
import { BattleType } from "#enums/battle-type";
|
||||||
import { GameModes } from "#enums/game-modes";
|
import { GameModes } from "#enums/game-modes";
|
||||||
import type { MoveId } from "#enums/move-id";
|
import type { MoveId } from "#enums/move-id";
|
||||||
@ -11,7 +10,7 @@ import type { SpeciesId } from "#enums/species-id";
|
|||||||
import { PlayerPokemon } from "#field/pokemon";
|
import { PlayerPokemon } from "#field/pokemon";
|
||||||
import type { StarterMoveset } from "#system/game-data";
|
import type { StarterMoveset } from "#system/game-data";
|
||||||
import type { Starter } from "#ui/starter-select-ui-handler";
|
import type { Starter } from "#ui/starter-select-ui-handler";
|
||||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils";
|
||||||
|
|
||||||
/** Function to convert Blob to string */
|
/** Function to convert Blob to string */
|
||||||
export function blobToString(blob) {
|
export function blobToString(blob) {
|
||||||
|
@ -1,43 +1,47 @@
|
|||||||
|
import { getOnelineDiffStr } from "#test/test-utils/string-utils";
|
||||||
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Matcher to check if an array contains exactly the given items, disregarding order.
|
* Matcher that checks if an array contains exactly the given items, disregarding order.
|
||||||
* @param received - The object to check. Should be an array of elements.
|
* @param received - The received value. Should be an array of elements
|
||||||
* @returns The result of the matching
|
* @param expected - The array to check equality with
|
||||||
|
* @returns Whether the matcher passed
|
||||||
*/
|
*/
|
||||||
export function toEqualArrayUnsorted(this: MatcherState, received: unknown, expected: unknown): SyncExpectationResult {
|
export function toEqualArrayUnsorted(
|
||||||
|
this: MatcherState,
|
||||||
|
received: unknown,
|
||||||
|
expected: unknown[],
|
||||||
|
): SyncExpectationResult {
|
||||||
if (!Array.isArray(received)) {
|
if (!Array.isArray(received)) {
|
||||||
return {
|
return {
|
||||||
pass: this.isNot,
|
pass: false,
|
||||||
message: () => `Expected an array, but got ${this.utils.stringify(received)}!`,
|
message: () => `Expected an array, but got ${this.utils.stringify(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(expected)) {
|
|
||||||
return {
|
|
||||||
pass: this.isNot,
|
|
||||||
message: () => `Expected to recieve an array, but got ${this.utils.stringify(expected)}!`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (received.length !== expected.length) {
|
if (received.length !== expected.length) {
|
||||||
return {
|
return {
|
||||||
pass: this.isNot,
|
pass: false,
|
||||||
message: () => `Expected to recieve array of length ${received.length}, but got ${expected.length}!`,
|
message: () => `Expected to receive array of length ${received.length}, but got ${expected.length} instead!`,
|
||||||
actual: received,
|
actual: received,
|
||||||
expected,
|
expected,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const gotSorted = received.slice().sort();
|
const actualSorted = received.slice().sort();
|
||||||
const wantSorted = expected.slice().sort();
|
const expectedSorted = expected.slice().sort();
|
||||||
const pass = this.equals(gotSorted, wantSorted, [...this.customTesters, this.utils.iterableEquality]);
|
const pass = this.equals(actualSorted, expectedSorted, [...this.customTesters, this.utils.iterableEquality]);
|
||||||
|
|
||||||
|
const actualStr = getOnelineDiffStr.call(this, actualSorted);
|
||||||
|
const expectedStr = getOnelineDiffStr.call(this, expectedSorted);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pass: this.isNot !== pass,
|
pass,
|
||||||
message: () =>
|
message: () =>
|
||||||
`Expected ${this.utils.stringify(received)} to exactly equal ${this.utils.stringify(expected)} without order!`,
|
pass
|
||||||
actual: gotSorted,
|
? `Expected ${actualStr} to NOT exactly equal ${expectedStr} without order, but it did!`
|
||||||
expected: wantSorted,
|
: `Expected ${actualStr} to exactly equal ${expectedStr} without order, but it didn't!`,
|
||||||
|
expected: expectedSorted,
|
||||||
|
actual: actualSorted,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
43
test/test-utils/matchers/to-have-ability-applied.ts
Normal file
43
test/test-utils/matchers/to-have-ability-applied.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
/* biome-ignore-start lint/correctness/noUnusedImports: tsdoc imports */
|
||||||
|
import type { Pokemon } from "#field/pokemon";
|
||||||
|
/* biome-ignore-end lint/correctness/noUnusedImports: tsdoc imports */
|
||||||
|
|
||||||
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
|
import { AbilityId } from "#enums/ability-id";
|
||||||
|
import { getEnumStr } from "#test/test-utils/string-utils";
|
||||||
|
import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||||
|
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matcher that checks if a {@linkcode Pokemon} has applied a specific {@linkcode AbilityId}.
|
||||||
|
* @param received - The object to check. Should be a {@linkcode Pokemon}
|
||||||
|
* @param expectedAbility - The {@linkcode AbilityId} to check for
|
||||||
|
* @returns Whether the matcher passed
|
||||||
|
*/
|
||||||
|
export function toHaveAbilityApplied(
|
||||||
|
this: MatcherState,
|
||||||
|
received: unknown,
|
||||||
|
expectedAbilityId: AbilityId,
|
||||||
|
): SyncExpectationResult {
|
||||||
|
if (!isPokemonInstance(received)) {
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
message: () => `Expected to recieve a Pokemon, but got ${receivedStr(received)}!`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const pass = received.waveData.abilitiesApplied.has(expectedAbilityId);
|
||||||
|
|
||||||
|
const pkmName = getPokemonNameWithAffix(received);
|
||||||
|
const expectedAbilityStr = getEnumStr(AbilityId, expectedAbilityId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: () =>
|
||||||
|
pass
|
||||||
|
? `Expected ${pkmName} to NOT have applied ${expectedAbilityStr}, but it did!`
|
||||||
|
: `Expected ${pkmName} to have applied ${expectedAbilityStr}, but it didn't!`,
|
||||||
|
expected: expectedAbilityId,
|
||||||
|
actual: received.waveData.abilitiesApplied,
|
||||||
|
};
|
||||||
|
}
|
43
test/test-utils/matchers/to-have-battler-tag.ts
Normal file
43
test/test-utils/matchers/to-have-battler-tag.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
/* biome-ignore-start lint/correctness/noUnusedImports: tsdoc imports */
|
||||||
|
import type { Pokemon } from "#field/pokemon";
|
||||||
|
/* biome-ignore-end lint/correctness/noUnusedImports: tsdoc imports */
|
||||||
|
|
||||||
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
|
import { getEnumStr } from "#test/test-utils/string-utils";
|
||||||
|
import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||||
|
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matcher that checks if a {@linkcode Pokemon} has a specific {@linkcode BattlerTagType}.
|
||||||
|
* @param received - The object to check. Should be a {@linkcode Pokemon}
|
||||||
|
* @param expectedBattlerTagType - The {@linkcode BattlerTagType} to check for
|
||||||
|
* @returns Whether the matcher passed
|
||||||
|
*/
|
||||||
|
export function toHaveBattlerTag(
|
||||||
|
this: MatcherState,
|
||||||
|
received: unknown,
|
||||||
|
expectedBattlerTagType: BattlerTagType,
|
||||||
|
): SyncExpectationResult {
|
||||||
|
if (!isPokemonInstance(received)) {
|
||||||
|
return {
|
||||||
|
pass: this.isNot,
|
||||||
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const pass = !!received.getTag(expectedBattlerTagType);
|
||||||
|
const pkmName = getPokemonNameWithAffix(received);
|
||||||
|
// "BattlerTagType.SEEDED (=1)"
|
||||||
|
const expectedTagStr = getEnumStr(BattlerTagType, expectedBattlerTagType, { prefix: "BattlerTagType." });
|
||||||
|
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: () =>
|
||||||
|
pass
|
||||||
|
? `Expected ${pkmName} to NOT have ${expectedTagStr}, but it did!`
|
||||||
|
: `Expected ${pkmName} to have ${expectedTagStr}, but it didn't!`,
|
||||||
|
expected: expectedBattlerTagType,
|
||||||
|
actual: received.summonData.tags.map(t => t.tagType),
|
||||||
|
};
|
||||||
|
}
|
66
test/test-utils/matchers/to-have-effective-stat.ts
Normal file
66
test/test-utils/matchers/to-have-effective-stat.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
|
import type { EffectiveStat } from "#enums/stat";
|
||||||
|
import type { Pokemon } from "#field/pokemon";
|
||||||
|
import type { Move } from "#moves/move";
|
||||||
|
import { getStatName } from "#test/test-utils/string-utils";
|
||||||
|
import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||||
|
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||||
|
|
||||||
|
export interface ToHaveEffectiveStatMatcherOptions {
|
||||||
|
/**
|
||||||
|
* The target {@linkcode Pokemon}
|
||||||
|
* @see {@linkcode Pokemon.getEffectiveStat}
|
||||||
|
*/
|
||||||
|
enemy?: Pokemon;
|
||||||
|
/**
|
||||||
|
* The {@linkcode Move} being used
|
||||||
|
* @see {@linkcode Pokemon.getEffectiveStat}
|
||||||
|
*/
|
||||||
|
move?: Move;
|
||||||
|
/**
|
||||||
|
* Whether a critical hit occurred or not
|
||||||
|
* @see {@linkcode Pokemon.getEffectiveStat}
|
||||||
|
* @defaultValue `false`
|
||||||
|
*/
|
||||||
|
isCritical?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matcher that checks if a {@linkcode Pokemon}'s effective stat equals a certain value.
|
||||||
|
* @param received - The object to check. Should be a {@linkcode Pokemon}
|
||||||
|
* @param stat - The {@linkcode EffectiveStat} to check
|
||||||
|
* @param expectedValue - The expected value of the {@linkcode stat}
|
||||||
|
* @param options - The {@linkcode ToHaveEffectiveStatMatcherOptions}
|
||||||
|
* @returns Whether the matcher passed
|
||||||
|
*/
|
||||||
|
export function toHaveEffectiveStat(
|
||||||
|
this: MatcherState,
|
||||||
|
received: unknown,
|
||||||
|
stat: EffectiveStat,
|
||||||
|
expectedValue: number,
|
||||||
|
{ enemy, move, isCritical = false }: ToHaveEffectiveStatMatcherOptions = {},
|
||||||
|
): SyncExpectationResult {
|
||||||
|
if (!isPokemonInstance(received)) {
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Change once getEffectiveStat is refactored to take an object literal
|
||||||
|
const actualValue = received.getEffectiveStat(stat, enemy, move, undefined, undefined, undefined, isCritical);
|
||||||
|
const pass = actualValue === expectedValue;
|
||||||
|
|
||||||
|
const pkmName = getPokemonNameWithAffix(received);
|
||||||
|
const statName = getStatName(stat);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: () =>
|
||||||
|
pass
|
||||||
|
? `Expected ${pkmName} to NOT have ${expectedValue} ${statName}, but it did!`
|
||||||
|
: `Expected ${pkmName} to have ${expectedValue} ${statName}, but got ${actualValue} instead!`,
|
||||||
|
expected: expectedValue,
|
||||||
|
actual: actualValue,
|
||||||
|
};
|
||||||
|
}
|
35
test/test-utils/matchers/to-have-fainted.ts
Normal file
35
test/test-utils/matchers/to-have-fainted.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
|
// biome-ignore lint/correctness/noUnusedImports: TSDoc
|
||||||
|
import type { Pokemon } from "#field/pokemon";
|
||||||
|
import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||||
|
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matcher that checks if a {@linkcode Pokemon} has fainted.
|
||||||
|
* @param received - The object to check. Should be a {@linkcode Pokemon}
|
||||||
|
* @returns Whether the matcher passed
|
||||||
|
*/
|
||||||
|
export function toHaveFainted(this: MatcherState, received: unknown): SyncExpectationResult {
|
||||||
|
if (!isPokemonInstance(received)) {
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const pass = received.isFainted();
|
||||||
|
|
||||||
|
const hp = received.hp;
|
||||||
|
const maxHp = received.getMaxHp();
|
||||||
|
const pkmName = getPokemonNameWithAffix(received);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: () =>
|
||||||
|
pass
|
||||||
|
? `Expected ${pkmName} to NOT have fainted, but it did!`
|
||||||
|
: `Expected ${pkmName} to have fainted, but it didn't! (${hp}/${maxHp} HP)`,
|
||||||
|
expected: 0,
|
||||||
|
actual: hp,
|
||||||
|
};
|
||||||
|
}
|
35
test/test-utils/matchers/to-have-full-hp.ts
Normal file
35
test/test-utils/matchers/to-have-full-hp.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
|
// biome-ignore lint/correctness/noUnusedImports: TSDoc
|
||||||
|
import type { Pokemon } from "#field/pokemon";
|
||||||
|
import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||||
|
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matcher that checks if a {@linkcode Pokemon} is at full hp.
|
||||||
|
* @param received - The object to check. Should be a {@linkcode Pokemon}.
|
||||||
|
* @returns Whether the matcher passed
|
||||||
|
*/
|
||||||
|
export function toHaveFullHp(this: MatcherState, received: unknown): SyncExpectationResult {
|
||||||
|
if (!isPokemonInstance(received)) {
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const pass = received.isFullHp();
|
||||||
|
|
||||||
|
const hp = received.hp;
|
||||||
|
const maxHp = received.getMaxHp();
|
||||||
|
const pkmName = getPokemonNameWithAffix(received);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: () =>
|
||||||
|
pass
|
||||||
|
? `Expected ${pkmName} to NOT have full hp, but it did!`
|
||||||
|
: `Expected ${pkmName} to have full hp, but it didn't! (${hp}/${maxHp} HP)`,
|
||||||
|
expected: maxHp,
|
||||||
|
actual: hp,
|
||||||
|
};
|
||||||
|
}
|
35
test/test-utils/matchers/to-have-hp.ts
Normal file
35
test/test-utils/matchers/to-have-hp.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
|
// biome-ignore lint/correctness/noUnusedImports: TSDoc
|
||||||
|
import type { Pokemon } from "#field/pokemon";
|
||||||
|
import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||||
|
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matcher that checks if a Pokemon has a specific amount of HP.
|
||||||
|
* @param received - The object to check. Should be a {@linkcode Pokemon}.
|
||||||
|
* @param expectedHp - The expected amount of HP the {@linkcode Pokemon} has
|
||||||
|
* @returns Whether the matcher passed
|
||||||
|
*/
|
||||||
|
export function toHaveHp(this: MatcherState, received: unknown, expectedHp: number): SyncExpectationResult {
|
||||||
|
if (!isPokemonInstance(received)) {
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const actualHp = received.hp;
|
||||||
|
const pass = actualHp === expectedHp;
|
||||||
|
|
||||||
|
const pkmName = getPokemonNameWithAffix(received);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: () =>
|
||||||
|
pass
|
||||||
|
? `Expected ${pkmName} to NOT have ${expectedHp} HP, but it did!`
|
||||||
|
: `Expected ${pkmName} to have ${expectedHp} HP, but got ${actualHp} HP instead!`,
|
||||||
|
expected: expectedHp,
|
||||||
|
actual: actualHp,
|
||||||
|
};
|
||||||
|
}
|
53
test/test-utils/matchers/to-have-stat-stage.ts
Normal file
53
test/test-utils/matchers/to-have-stat-stage.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/** biome-ignore-start lint/correctness/noUnusedImports: TSDoc imports */
|
||||||
|
import type { Pokemon } from "#field/pokemon";
|
||||||
|
/** biome-ignore-end lint/correctness/noUnusedImports: TSDoc imports */
|
||||||
|
|
||||||
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
|
import type { BattleStat } from "#enums/stat";
|
||||||
|
import { getStatName } from "#test/test-utils/string-utils";
|
||||||
|
import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||||
|
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matcher that checks if a Pokemon has a specific {@linkcode BattleStat | Stat} stage.
|
||||||
|
* @param received - The object to check. Should be a {@linkcode Pokemon}.
|
||||||
|
* @param stat - The {@linkcode BattleStat | Stat} to check
|
||||||
|
* @param expectedStage - The expected numerical value of {@linkcode stat}; should be within the range `[-6, 6]`
|
||||||
|
* @returns Whether the matcher passed
|
||||||
|
*/
|
||||||
|
export function toHaveStatStage(
|
||||||
|
this: MatcherState,
|
||||||
|
received: unknown,
|
||||||
|
stat: BattleStat,
|
||||||
|
expectedStage: number,
|
||||||
|
): SyncExpectationResult {
|
||||||
|
if (!isPokemonInstance(received)) {
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expectedStage < -6 || expectedStage > 6) {
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
message: () => `Expected ${expectedStage} to be within the range [-6, 6]!`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const actualStage = received.getStatStage(stat);
|
||||||
|
const pass = actualStage === expectedStage;
|
||||||
|
|
||||||
|
const pkmName = getPokemonNameWithAffix(received);
|
||||||
|
const statName = getStatName(stat);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: () =>
|
||||||
|
pass
|
||||||
|
? `Expected ${pkmName}'s ${statName} stat stage to NOT be ${expectedStage}, but it was!`
|
||||||
|
: `Expected ${pkmName}'s ${statName} stat stage to be ${expectedStage}, but got ${actualStage} instead!`,
|
||||||
|
expected: expectedStage,
|
||||||
|
actual: actualStage,
|
||||||
|
};
|
||||||
|
}
|
83
test/test-utils/matchers/to-have-status-effect.ts
Normal file
83
test/test-utils/matchers/to-have-status-effect.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
/* biome-ignore-start lint/correctness/noUnusedImports: tsdoc imports */
|
||||||
|
import type { Status } from "#data/status-effect";
|
||||||
|
import type { Pokemon } from "#field/pokemon";
|
||||||
|
/* biome-ignore-end lint/correctness/noUnusedImports: tsdoc imports */
|
||||||
|
|
||||||
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
|
import { getEnumStr, getOnelineDiffStr } from "#test/test-utils/string-utils";
|
||||||
|
import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||||
|
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||||
|
|
||||||
|
export type expectedStatusType =
|
||||||
|
| StatusEffect
|
||||||
|
| { effect: StatusEffect.TOXIC; toxicTurnCount: number }
|
||||||
|
| { effect: StatusEffect.SLEEP; sleepTurnsRemaining: number };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matcher that checks if a Pokemon's {@linkcode StatusEffect} is as expected
|
||||||
|
* @param received - The actual value received. Should be a {@linkcode Pokemon}
|
||||||
|
* @param expectedStatus - The {@linkcode StatusEffect} the Pokemon is expected to have,
|
||||||
|
* or a partially filled {@linkcode Status} containing the desired properties
|
||||||
|
* @returns Whether the matcher passed
|
||||||
|
*/
|
||||||
|
export function toHaveStatusEffect(
|
||||||
|
this: MatcherState,
|
||||||
|
received: unknown,
|
||||||
|
expectedStatus: expectedStatusType,
|
||||||
|
): SyncExpectationResult {
|
||||||
|
if (!isPokemonInstance(received)) {
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const pkmName = getPokemonNameWithAffix(received);
|
||||||
|
const actualEffect = received.status?.effect ?? StatusEffect.NONE;
|
||||||
|
|
||||||
|
// Check exclusively effect equality first, coercing non-matching status effects to numbers.
|
||||||
|
if (actualEffect !== (expectedStatus as Exclude<typeof expectedStatus, StatusEffect>)?.effect) {
|
||||||
|
// This is actually 100% safe as `expectedStatus?.effect` will evaluate to `undefined` if a StatusEffect was passed,
|
||||||
|
// which will never match actualEffect by definition
|
||||||
|
expectedStatus = (expectedStatus as Exclude<typeof expectedStatus, StatusEffect>).effect;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof expectedStatus === "number") {
|
||||||
|
const pass = this.equals(actualEffect, expectedStatus, [...this.customTesters, this.utils.iterableEquality]);
|
||||||
|
|
||||||
|
const actualStr = getEnumStr(StatusEffect, actualEffect, { prefix: "StatusEffect." });
|
||||||
|
const expectedStr = getEnumStr(StatusEffect, expectedStatus, { prefix: "StatusEffect." });
|
||||||
|
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: () =>
|
||||||
|
pass
|
||||||
|
? `Expected ${pkmName} to NOT have ${expectedStr}, but it did!`
|
||||||
|
: `Expected ${pkmName} to have status effect ${expectedStr}, but got ${actualStr} instead!`,
|
||||||
|
expected: expectedStatus,
|
||||||
|
actual: actualEffect,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for equality of all fields (for toxic turn count/etc)
|
||||||
|
const actualStatus = received.status;
|
||||||
|
const pass = this.equals(received, expectedStatus, [
|
||||||
|
...this.customTesters,
|
||||||
|
this.utils.subsetEquality,
|
||||||
|
this.utils.iterableEquality,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const expectedStr = getOnelineDiffStr.call(this, expectedStatus);
|
||||||
|
const actualStr = getOnelineDiffStr.call(this, actualStatus);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: () =>
|
||||||
|
pass
|
||||||
|
? `Expected ${pkmName}'s status to NOT match ${expectedStr}, but it did!`
|
||||||
|
: `Expected ${pkmName}'s status to match ${expectedStr}, but got ${actualStr} instead!`,
|
||||||
|
expected: expectedStatus,
|
||||||
|
actual: actualStatus,
|
||||||
|
};
|
||||||
|
}
|
46
test/test-utils/matchers/to-have-taken-damage.ts
Normal file
46
test/test-utils/matchers/to-have-taken-damage.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
/** biome-ignore-start lint/correctness/noUnusedImports: TSDoc imports */
|
||||||
|
import type { Pokemon } from "#field/pokemon";
|
||||||
|
/** biome-ignore-end lint/correctness/noUnusedImports: TSDoc imports */
|
||||||
|
|
||||||
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
|
import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||||
|
import { toDmgValue } from "#utils/common";
|
||||||
|
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matcher that checks if a Pokemon has taken a specific amount of damage.
|
||||||
|
* Unless specified, will run the expected damage value through {@linkcode toDmgValue}
|
||||||
|
* to round it down and make it a minimum of 1.
|
||||||
|
* @param received - The object to check. Should be a {@linkcode Pokemon}.
|
||||||
|
* @param expectedDamageTaken - The expected amount of damage the {@linkcode Pokemon} has taken
|
||||||
|
* @param roundDown - Whether to round down {@linkcode expectedDamageTaken} with {@linkcode toDmgValue}; default `true`
|
||||||
|
* @returns Whether the matcher passed
|
||||||
|
*/
|
||||||
|
export function toHaveTakenDamage(
|
||||||
|
this: MatcherState,
|
||||||
|
received: unknown,
|
||||||
|
expectedDamageTaken: number,
|
||||||
|
roundDown = true,
|
||||||
|
): SyncExpectationResult {
|
||||||
|
if (!isPokemonInstance(received)) {
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedDmgValue = roundDown ? toDmgValue(expectedDamageTaken) : expectedDamageTaken;
|
||||||
|
const actualDmgValue = received.getInverseHp();
|
||||||
|
const pass = actualDmgValue === expectedDmgValue;
|
||||||
|
const pkmName = getPokemonNameWithAffix(received);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: () =>
|
||||||
|
pass
|
||||||
|
? `Expected ${pkmName} to NOT have taken ${expectedDmgValue} damage, but it did!`
|
||||||
|
: `Expected ${pkmName} to have taken ${expectedDmgValue} damage, but got ${actualDmgValue} instead!`,
|
||||||
|
expected: expectedDmgValue,
|
||||||
|
actual: actualDmgValue,
|
||||||
|
};
|
||||||
|
}
|
62
test/test-utils/matchers/to-have-terrain.ts
Normal file
62
test/test-utils/matchers/to-have-terrain.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
/** biome-ignore-start lint/correctness/noUnusedImports: TSDoc imports */
|
||||||
|
import type { GameManager } from "#test/test-utils/game-manager";
|
||||||
|
/** biome-ignore-end lint/correctness/noUnusedImports: TSDoc imports */
|
||||||
|
|
||||||
|
import { TerrainType } from "#app/data/terrain";
|
||||||
|
import { getEnumStr } from "#test/test-utils/string-utils";
|
||||||
|
import { isGameManagerInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||||
|
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matcher that checks if the {@linkcode TerrainType} is as expected
|
||||||
|
* @param received - The object to check. Should be an instance of {@linkcode GameManager}.
|
||||||
|
* @param expectedTerrainType - The expected {@linkcode TerrainType}, or {@linkcode TerrainType.NONE} if no terrain should be active
|
||||||
|
* @returns Whether the matcher passed
|
||||||
|
*/
|
||||||
|
export function toHaveTerrain(
|
||||||
|
this: MatcherState,
|
||||||
|
received: unknown,
|
||||||
|
expectedTerrainType: TerrainType,
|
||||||
|
): SyncExpectationResult {
|
||||||
|
if (!isGameManagerInstance(received)) {
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
message: () => `Expected GameManager, but got ${receivedStr(received)}!`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!received.scene?.arena) {
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
message: () => `Expected GameManager.${received.scene ? "scene" : "scene.arena"} to be defined!`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const actual = received.scene.arena.getTerrainType();
|
||||||
|
const pass = actual === expectedTerrainType;
|
||||||
|
const actualStr = toTerrainStr(actual);
|
||||||
|
const expectedStr = toTerrainStr(expectedTerrainType);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: () =>
|
||||||
|
pass
|
||||||
|
? `Expected Arena to NOT have ${expectedStr} active, but it did!`
|
||||||
|
: `Expected Arena to have ${expectedStr} active, but got ${actualStr} instead!`,
|
||||||
|
expected: expectedTerrainType,
|
||||||
|
actual,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a human readable string of the current {@linkcode TerrainType}.
|
||||||
|
* @param terrainType - The {@linkcode TerrainType} to transform
|
||||||
|
* @returns A human readable string
|
||||||
|
*/
|
||||||
|
function toTerrainStr(terrainType: TerrainType) {
|
||||||
|
if (terrainType === TerrainType.NONE) {
|
||||||
|
return "no terrain";
|
||||||
|
}
|
||||||
|
// "Electric Terrain (=2)"
|
||||||
|
return getEnumStr(TerrainType, terrainType, { casing: "Title", suffix: " Terrain" });
|
||||||
|
}
|
@ -1,6 +1,9 @@
|
|||||||
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
import { PokemonType } from "#enums/pokemon-type";
|
import { PokemonType } from "#enums/pokemon-type";
|
||||||
import { Pokemon } from "#field/pokemon";
|
import type { Pokemon } from "#field/pokemon";
|
||||||
|
import { stringifyEnumArray } from "#test/test-utils/string-utils";
|
||||||
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||||
|
import { isPokemonInstance, receivedStr } from "../test-utils";
|
||||||
|
|
||||||
export interface toHaveTypesOptions {
|
export interface toHaveTypesOptions {
|
||||||
/**
|
/**
|
||||||
@ -15,7 +18,7 @@ export interface toHaveTypesOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Matcher to check if an array contains exactly the given items, disregarding order.
|
* Matcher that checks if an array contains exactly the given items, disregarding order.
|
||||||
* @param received - The object to check. Should be an array of one or more {@linkcode PokemonType}s.
|
* @param received - The object to check. Should be an array of one or more {@linkcode PokemonType}s.
|
||||||
* @param options - The {@linkcode toHaveTypesOptions | options} for this matcher
|
* @param options - The {@linkcode toHaveTypesOptions | options} for this matcher
|
||||||
* @returns The result of the matching
|
* @returns The result of the matching
|
||||||
@ -23,42 +26,36 @@ export interface toHaveTypesOptions {
|
|||||||
export function toHaveTypes(
|
export function toHaveTypes(
|
||||||
this: MatcherState,
|
this: MatcherState,
|
||||||
received: unknown,
|
received: unknown,
|
||||||
expected: unknown,
|
expected: [PokemonType, ...PokemonType[]],
|
||||||
options: toHaveTypesOptions = {},
|
options: toHaveTypesOptions = {},
|
||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!(received instanceof Pokemon)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: this.isNot,
|
pass: false,
|
||||||
message: () => `Expected a Pokemon, but got ${this.utils.stringify(received)}!`,
|
message: () => `Expected to recieve a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(expected) || expected.length === 0) {
|
const actualTypes = received.getTypes(...(options.args ?? [])).sort();
|
||||||
return {
|
const expectedTypes = expected.slice().sort();
|
||||||
pass: this.isNot,
|
|
||||||
message: () => `Expected to recieve an array with length >=1, but got ${this.utils.stringify(expected)}!`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!expected.every((t): t is PokemonType => t in PokemonType)) {
|
// Exact matches do not care about subset equality
|
||||||
return {
|
const matchers = options.exact
|
||||||
pass: this.isNot,
|
? [...this.customTesters, this.utils.iterableEquality]
|
||||||
message: () => `Expected to recieve array of PokemonTypes but got ${this.utils.stringify(expected)}!`,
|
: [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality];
|
||||||
};
|
const pass = this.equals(actualTypes, expectedTypes, matchers);
|
||||||
}
|
|
||||||
|
|
||||||
const gotSorted = pkmnTypeToStr(received.getTypes(...(options.args ?? [])));
|
const actualStr = stringifyEnumArray(PokemonType, actualTypes);
|
||||||
const wantSorted = pkmnTypeToStr(expected.slice());
|
const expectedStr = stringifyEnumArray(PokemonType, expectedTypes);
|
||||||
const pass = this.equals(gotSorted, wantSorted, [...this.customTesters, this.utils.iterableEquality]);
|
const pkmName = getPokemonNameWithAffix(received);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pass: this.isNot !== pass,
|
pass,
|
||||||
message: () => `Expected ${received.name} to have types ${this.utils.stringify(wantSorted)}, but got ${gotSorted}!`,
|
message: () =>
|
||||||
actual: gotSorted,
|
pass
|
||||||
expected: wantSorted,
|
? `Expected ${pkmName} to NOT have types ${expectedStr}, but it did!`
|
||||||
|
: `Expected ${pkmName} to have types ${expectedStr}, but got ${actualStr} instead!`,
|
||||||
|
expected: expectedTypes,
|
||||||
|
actual: actualTypes,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function pkmnTypeToStr(p: PokemonType[]): string[] {
|
|
||||||
return p.sort().map(type => PokemonType[type]);
|
|
||||||
}
|
|
||||||
|
70
test/test-utils/matchers/to-have-used-move.ts
Normal file
70
test/test-utils/matchers/to-have-used-move.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
/** biome-ignore-start lint/correctness/noUnusedImports: TSDoc imports */
|
||||||
|
import type { Pokemon } from "#field/pokemon";
|
||||||
|
/** biome-ignore-end lint/correctness/noUnusedImports: TSDoc imports */
|
||||||
|
|
||||||
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
|
import type { MoveId } from "#enums/move-id";
|
||||||
|
import { getOnelineDiffStr, getOrdinal } from "#test/test-utils/string-utils";
|
||||||
|
import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||||
|
import type { TurnMove } from "#types/turn-move";
|
||||||
|
import type { AtLeastOne } from "#types/type-helpers";
|
||||||
|
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matcher to check the contents of a {@linkcode Pokemon}'s move history.
|
||||||
|
* @param received - The actual value received. Should be a {@linkcode Pokemon}
|
||||||
|
* @param expectedValue - The {@linkcode MoveId} the Pokemon is expected to have used,
|
||||||
|
* or a partially filled {@linkcode TurnMove} containing the desired properties to check
|
||||||
|
* @param index - The index of the move history entry to check, in order from most recent to least recent.
|
||||||
|
* Default `0` (last used move)
|
||||||
|
* @returns Whether the matcher passed
|
||||||
|
*/
|
||||||
|
export function toHaveUsedMove(
|
||||||
|
this: MatcherState,
|
||||||
|
received: unknown,
|
||||||
|
expectedResult: MoveId | AtLeastOne<TurnMove>,
|
||||||
|
index = 0,
|
||||||
|
): SyncExpectationResult {
|
||||||
|
if (!isPokemonInstance(received)) {
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const move: TurnMove | undefined = received.getLastXMoves(-1)[index];
|
||||||
|
const pkmName = getPokemonNameWithAffix(received);
|
||||||
|
|
||||||
|
if (move === undefined) {
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
message: () => `Expected ${pkmName} to have used ${index + 1} moves, but it didn't!`,
|
||||||
|
actual: received.getLastXMoves(-1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coerce to a `TurnMove`
|
||||||
|
if (typeof expectedResult === "number") {
|
||||||
|
expectedResult = { move: expectedResult };
|
||||||
|
}
|
||||||
|
|
||||||
|
const moveIndexStr = index === 0 ? "last move" : `${getOrdinal(index)} most recent move`;
|
||||||
|
|
||||||
|
const pass = this.equals(move, expectedResult, [
|
||||||
|
...this.customTesters,
|
||||||
|
this.utils.subsetEquality,
|
||||||
|
this.utils.iterableEquality,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const expectedStr = getOnelineDiffStr.call(this, expectedResult);
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: () =>
|
||||||
|
pass
|
||||||
|
? `Expected ${pkmName}'s ${moveIndexStr} to NOT match ${expectedStr}, but it did!`
|
||||||
|
: // Replace newlines with spaces to preserve one-line ness
|
||||||
|
`Expected ${pkmName}'s ${moveIndexStr} to match ${expectedStr}, but it didn't!`,
|
||||||
|
expected: expectedResult,
|
||||||
|
actual: move,
|
||||||
|
};
|
||||||
|
}
|
77
test/test-utils/matchers/to-have-used-pp.ts
Normal file
77
test/test-utils/matchers/to-have-used-pp.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
// biome-ignore-start lint/correctness/noUnusedImports: TSDoc imports
|
||||||
|
import type { Pokemon } from "#field/pokemon";
|
||||||
|
// biome-ignore-end lint/correctness/noUnusedImports: TSDoc imports
|
||||||
|
|
||||||
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
|
import Overrides from "#app/overrides";
|
||||||
|
import { MoveId } from "#enums/move-id";
|
||||||
|
import { getEnumStr } from "#test/test-utils/string-utils";
|
||||||
|
import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||||
|
import { coerceArray } from "#utils/common";
|
||||||
|
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matcher to check the amount of PP consumed by a {@linkcode Pokemon}.
|
||||||
|
* @param received - The actual value received. Should be a {@linkcode Pokemon}
|
||||||
|
* @param expectedValue - The {@linkcode MoveId} that should have consumed PP
|
||||||
|
* @param ppUsed - The numerical amount of PP that should have been consumed,
|
||||||
|
* or `all` to indicate the move should be _out_ of PP
|
||||||
|
* @returns Whether the matcher passed
|
||||||
|
* @remarks
|
||||||
|
* If the same move appears in the Pokemon's moveset multiple times, this will fail the test!
|
||||||
|
*/
|
||||||
|
export function toHaveUsedPP(
|
||||||
|
this: MatcherState,
|
||||||
|
received: unknown,
|
||||||
|
expectedMove: MoveId,
|
||||||
|
ppUsed: number | "all",
|
||||||
|
): SyncExpectationResult {
|
||||||
|
if (!isPokemonInstance(received)) {
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const override = received.isPlayer() ? Overrides.MOVESET_OVERRIDE : Overrides.OPP_MOVESET_OVERRIDE;
|
||||||
|
if (coerceArray(override).length > 0) {
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
message: () =>
|
||||||
|
`Cannot test for PP consumption with ${received.isPlayer() ? "player" : "enemy"} moveset overrides active!`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const pkmName = getPokemonNameWithAffix(received);
|
||||||
|
const moveStr = getEnumStr(MoveId, expectedMove);
|
||||||
|
|
||||||
|
const movesetMoves = received.getMoveset().filter(pm => pm.moveId === expectedMove);
|
||||||
|
if (movesetMoves.length !== 1) {
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
message: () =>
|
||||||
|
`Expected MoveId.${moveStr} to appear in ${pkmName}'s moveset exactly once, but got ${movesetMoves.length} times!`,
|
||||||
|
expected: expectedMove,
|
||||||
|
actual: received.getMoveset(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const move = movesetMoves[0]; // will be the only move in the array
|
||||||
|
|
||||||
|
let ppStr: string = ppUsed.toString();
|
||||||
|
if (typeof ppUsed === "string") {
|
||||||
|
ppStr = "all its";
|
||||||
|
ppUsed = move.getMovePp();
|
||||||
|
}
|
||||||
|
const pass = move.ppUsed === ppUsed;
|
||||||
|
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: () =>
|
||||||
|
pass
|
||||||
|
? `Expected ${pkmName}'s ${moveStr} to NOT have used ${ppStr} PP, but it did!`
|
||||||
|
: `Expected ${pkmName}'s ${moveStr} to have used ${ppStr} PP, but got ${move.ppUsed} instead!`,
|
||||||
|
expected: ppUsed,
|
||||||
|
actual: move.ppUsed,
|
||||||
|
};
|
||||||
|
}
|
62
test/test-utils/matchers/to-have-weather.ts
Normal file
62
test/test-utils/matchers/to-have-weather.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
/** biome-ignore-start lint/correctness/noUnusedImports: TSDoc imports */
|
||||||
|
import type { GameManager } from "#test/test-utils/game-manager";
|
||||||
|
/** biome-ignore-end lint/correctness/noUnusedImports: TSDoc imports */
|
||||||
|
|
||||||
|
import { WeatherType } from "#enums/weather-type";
|
||||||
|
import { isGameManagerInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||||
|
import { toTitleCase } from "#utils/strings";
|
||||||
|
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matcher that checks if the {@linkcode WeatherType} is as expected
|
||||||
|
* @param received - The object to check. Expects an instance of {@linkcode GameManager}.
|
||||||
|
* @param expectedWeatherType - The expected {@linkcode WeatherType}
|
||||||
|
* @returns Whether the matcher passed
|
||||||
|
*/
|
||||||
|
export function toHaveWeather(
|
||||||
|
this: MatcherState,
|
||||||
|
received: unknown,
|
||||||
|
expectedWeatherType: WeatherType,
|
||||||
|
): SyncExpectationResult {
|
||||||
|
if (!isGameManagerInstance(received)) {
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
message: () => `Expected GameManager, but got ${receivedStr(received)}!`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!received.scene?.arena) {
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
message: () => `Expected GameManager.${received.scene ? "scene" : "scene.arena"} to be defined!`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const actual = received.scene.arena.getWeatherType();
|
||||||
|
const pass = actual === expectedWeatherType;
|
||||||
|
const actualStr = toWeatherStr(actual);
|
||||||
|
const expectedStr = toWeatherStr(expectedWeatherType);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: () =>
|
||||||
|
pass
|
||||||
|
? `Expected Arena to NOT have ${expectedStr} weather active, but it did!`
|
||||||
|
: `Expected Arena to have ${expectedStr} weather active, but got ${actualStr} instead!`,
|
||||||
|
expected: expectedWeatherType,
|
||||||
|
actual,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a human readable representation of the current {@linkcode WeatherType}.
|
||||||
|
* @param weatherType - The {@linkcode WeatherType} to transform
|
||||||
|
* @returns A human readable string
|
||||||
|
*/
|
||||||
|
function toWeatherStr(weatherType: WeatherType) {
|
||||||
|
if (weatherType === WeatherType.NONE) {
|
||||||
|
return "no weather";
|
||||||
|
}
|
||||||
|
|
||||||
|
return toTitleCase(WeatherType[weatherType]);
|
||||||
|
}
|
183
test/test-utils/string-utils.ts
Normal file
183
test/test-utils/string-utils.ts
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
import { getStatKey, type Stat } from "#enums/stat";
|
||||||
|
import type { EnumOrObject, NormalEnum, TSNumericEnum } from "#types/enum-types";
|
||||||
|
import type { ObjectValues } from "#types/type-helpers";
|
||||||
|
import { enumValueToKey } from "#utils/enums";
|
||||||
|
import { toTitleCase } from "#utils/strings";
|
||||||
|
import type { MatcherState } from "@vitest/expect";
|
||||||
|
import i18next from "i18next";
|
||||||
|
|
||||||
|
type Casing = "Preserve" | "Title";
|
||||||
|
|
||||||
|
interface getEnumStrOptions {
|
||||||
|
/**
|
||||||
|
* A string denoting the casing method to use.
|
||||||
|
* @defaultValue "Preserve"
|
||||||
|
*/
|
||||||
|
casing?: Casing;
|
||||||
|
/**
|
||||||
|
* If present, will be prepended to the beginning of the enum string.
|
||||||
|
*/
|
||||||
|
prefix?: string;
|
||||||
|
/**
|
||||||
|
* If present, will be added to the end of the enum string.
|
||||||
|
*/
|
||||||
|
suffix?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the name of an enum member or const object value, alongside its corresponding value.
|
||||||
|
* @param obj - The {@linkcode EnumOrObject} to source reverse mappings from
|
||||||
|
* @param enums - One of {@linkcode obj}'s values
|
||||||
|
* @param casing - A string denoting the casing method to use; default `Preserve`
|
||||||
|
* @param prefix - An optional string to be prepended to the enum's string representation
|
||||||
|
* @param suffix - An optional string to be appended to the enum's string representation
|
||||||
|
* @returns The stringified representation of `val` as dictated by the options.
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* enum fakeEnum {
|
||||||
|
* ONE: 1,
|
||||||
|
* TWO: 2,
|
||||||
|
* THREE: 3,
|
||||||
|
* }
|
||||||
|
* getEnumStr(fakeEnum, fakeEnum.ONE); // Output: "ONE (=1)"
|
||||||
|
* getEnumStr(fakeEnum, fakeEnum.TWO, {casing: "Title", prefix: "fakeEnum.", suffix: "!!!"}); // Output: "fakeEnum.TWO!!! (=2)"
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function getEnumStr<E extends EnumOrObject>(
|
||||||
|
obj: E,
|
||||||
|
val: ObjectValues<E>,
|
||||||
|
{ casing = "Preserve", prefix = "", suffix = "" }: getEnumStrOptions = {},
|
||||||
|
): string {
|
||||||
|
let casingFunc: ((s: string) => string) | undefined;
|
||||||
|
switch (casing) {
|
||||||
|
case "Preserve":
|
||||||
|
break;
|
||||||
|
case "Title":
|
||||||
|
casingFunc = toTitleCase;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stringPart =
|
||||||
|
obj[val] !== undefined
|
||||||
|
? // TS reverse mapped enum
|
||||||
|
(obj[val] as string)
|
||||||
|
: // Normal enum/`const object`
|
||||||
|
(enumValueToKey(obj as NormalEnum<E>, val) as string);
|
||||||
|
|
||||||
|
if (casingFunc) {
|
||||||
|
stringPart = casingFunc(stringPart);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${prefix}${stringPart}${suffix} (=${val})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert an array of enums or `const object`s into a readable string version.
|
||||||
|
* @param obj - The {@linkcode EnumOrObject} to source reverse mappings from
|
||||||
|
* @param enums - An array of {@linkcode obj}'s values
|
||||||
|
* @returns The stringified representation of `enums`.
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* enum fakeEnum {
|
||||||
|
* ONE: 1,
|
||||||
|
* TWO: 2,
|
||||||
|
* THREE: 3,
|
||||||
|
* }
|
||||||
|
* console.log(stringifyEnumArray(fakeEnum, [fakeEnum.ONE, fakeEnum.TWO, fakeEnum.THREE])); // Output: "[ONE, TWO, THREE] (=[1, 2, 3])"
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function stringifyEnumArray<E extends EnumOrObject>(obj: E, enums: E[keyof E][]): string {
|
||||||
|
if (obj.length === 0) {
|
||||||
|
return "[]";
|
||||||
|
}
|
||||||
|
|
||||||
|
const vals = enums.slice();
|
||||||
|
/** An array of string names */
|
||||||
|
let names: string[];
|
||||||
|
|
||||||
|
if (obj[enums[0]] !== undefined) {
|
||||||
|
// Reverse mapping exists - `obj` is a `TSNumericEnum` and its reverse mapped counterparts are strings
|
||||||
|
names = enums.map(e => (obj as TSNumericEnum<E>)[e] as string);
|
||||||
|
} else {
|
||||||
|
// No reverse mapping exists means `obj` is a `NormalEnum`.
|
||||||
|
// NB: This (while ugly) should be more ergonomic than doing a repeated lookup for large `const object`s
|
||||||
|
// as the `enums` array should be significantly shorter than the corresponding enum type.
|
||||||
|
names = [];
|
||||||
|
for (const [k, v] of Object.entries(obj as NormalEnum<E>)) {
|
||||||
|
if (names.length === enums.length) {
|
||||||
|
// No more names to get
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Find all matches for the given enum, assigning their keys to the names array
|
||||||
|
findIndices(enums, v).forEach(matchIndex => {
|
||||||
|
names[matchIndex] = k;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `[${names.join(", ")}] (=[${vals.join(", ")}])`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the indices of all occurrences of a value in an array.
|
||||||
|
* @param arr - The array to search
|
||||||
|
* @param searchElement - The value to locate in the array
|
||||||
|
* @param fromIndex - The array index at which to begin the search. If fromIndex is omitted, the
|
||||||
|
* search starts at index 0
|
||||||
|
*/
|
||||||
|
function findIndices<T>(arr: T[], searchElement: T, fromIndex = 0): number[] {
|
||||||
|
const indices: number[] = [];
|
||||||
|
const arrSliced = arr.slice(fromIndex);
|
||||||
|
for (const [index, value] of arrSliced.entries()) {
|
||||||
|
if (value === searchElement) {
|
||||||
|
indices.push(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return indices;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a number into an English ordinal
|
||||||
|
* @param num - The number to convert into an ordinal
|
||||||
|
* @returns The ordinal representation of {@linkcode num}.
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* console.log(getOrdinal(1)); // Output: "1st"
|
||||||
|
* console.log(getOrdinal(12)); // Output: "12th"
|
||||||
|
* console.log(getOrdinal(24)); // Output: "24th"
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function getOrdinal(num: number): string {
|
||||||
|
const tens = num % 10;
|
||||||
|
const hundreds = num % 100;
|
||||||
|
if (tens === 1 && hundreds !== 11) {
|
||||||
|
return num + "st";
|
||||||
|
}
|
||||||
|
if (tens === 2 && hundreds !== 12) {
|
||||||
|
return num + "nd";
|
||||||
|
}
|
||||||
|
if (tens === 3 && hundreds !== 13) {
|
||||||
|
return num + "rd";
|
||||||
|
}
|
||||||
|
return num + "th";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the localized name of a {@linkcode Stat}.
|
||||||
|
* @param s - The {@linkcode Stat} to check
|
||||||
|
* @returns - The proper name for s, retrieved from the translations.
|
||||||
|
*/
|
||||||
|
export function getStatName(s: Stat): string {
|
||||||
|
return i18next.t(getStatKey(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert an object into a oneline diff to be shown in an error message.
|
||||||
|
* @param obj - The object to return the oneline diff of
|
||||||
|
* @returns The updated diff
|
||||||
|
*/
|
||||||
|
export function getOnelineDiffStr(this: MatcherState, obj: unknown): string {
|
||||||
|
return this.utils
|
||||||
|
.stringify(obj, undefined, { maxLength: 35, indent: 0, printBasicPrototype: false })
|
||||||
|
.replace(/\n/g, " ") // Replace newlines with spaces
|
||||||
|
.replace(/,(\s*)}$/g, "$1}");
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Pokemon } from "#field/pokemon";
|
||||||
|
import type { GameManager } from "#test/test-utils/game-manager";
|
||||||
import i18next, { type ParseKeys } from "i18next";
|
import i18next, { type ParseKeys } from "i18next";
|
||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
|
|
||||||
@ -29,3 +31,54 @@ export function arrayOfRange(start: number, end: number) {
|
|||||||
export function getApiBaseUrl() {
|
export function getApiBaseUrl() {
|
||||||
return import.meta.env.VITE_SERVER_URL ?? "http://localhost:8001";
|
return import.meta.env.VITE_SERVER_URL ?? "http://localhost:8001";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TypeOfResult = "undefined" | "object" | "boolean" | "number" | "bigint" | "string" | "symbol" | "function";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to determine the actual type of the received object as human readable string
|
||||||
|
* @param received - The received object
|
||||||
|
* @returns A human readable string of the received object (type)
|
||||||
|
*/
|
||||||
|
export function receivedStr(received: unknown, expectedType: TypeOfResult = "object"): string {
|
||||||
|
if (received === null) {
|
||||||
|
return "null";
|
||||||
|
}
|
||||||
|
if (received === undefined) {
|
||||||
|
return "undefined";
|
||||||
|
}
|
||||||
|
if (typeof received !== expectedType) {
|
||||||
|
return typeof received;
|
||||||
|
}
|
||||||
|
if (expectedType === "object") {
|
||||||
|
return received.constructor.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to check if the received object is an {@linkcode object}
|
||||||
|
* @param received - The object to check
|
||||||
|
* @returns Whether the object is an {@linkcode object}.
|
||||||
|
*/
|
||||||
|
function isObject(received: unknown): received is object {
|
||||||
|
return received !== null && typeof received === "object";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to check if a given object is a {@linkcode Pokemon}.
|
||||||
|
* @param received - The object to check
|
||||||
|
* @return Whether `received` is a {@linkcode Pokemon} instance.
|
||||||
|
*/
|
||||||
|
export function isPokemonInstance(received: unknown): received is Pokemon {
|
||||||
|
return isObject(received) && received instanceof Pokemon;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if an object is a {@linkcode GameManager} instance
|
||||||
|
* @param received - The object to check
|
||||||
|
* @returns Whether the object is a {@linkcode GameManager} instance.
|
||||||
|
*/
|
||||||
|
export function isGameManagerInstance(received: unknown): received is GameManager {
|
||||||
|
return isObject(received) && (received as GameManager).constructor.name === "GameManager";
|
||||||
|
}
|
||||||
|
38
test/types/type-helpers.test-d.ts
Normal file
38
test/types/type-helpers.test-d.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import type { AtLeastOne } from "#types/type-helpers";
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
import { expectTypeOf } from "vitest";
|
||||||
|
|
||||||
|
type fakeObj = {
|
||||||
|
foo: number;
|
||||||
|
bar: string;
|
||||||
|
baz: number | string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type optionalObj = {
|
||||||
|
foo: number;
|
||||||
|
bar: string;
|
||||||
|
baz?: number | string;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("AtLeastOne", () => {
|
||||||
|
it("should accept an object with at least 1 of its defined parameters", () => {
|
||||||
|
expectTypeOf<{ foo: number }>().toExtend<AtLeastOne<fakeObj>>();
|
||||||
|
expectTypeOf<{ bar: string }>().toExtend<AtLeastOne<fakeObj>>();
|
||||||
|
expectTypeOf<{ baz: number | string }>().toExtend<AtLeastOne<fakeObj>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should convert to a partial intersection with the union of all individual single properties", () => {
|
||||||
|
expectTypeOf<AtLeastOne<fakeObj>>().branded.toEqualTypeOf<
|
||||||
|
Partial<fakeObj> & ({ foo: number } | { bar: string } | { baz: number | string })
|
||||||
|
>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should treat optional properties as required", () => {
|
||||||
|
expectTypeOf<AtLeastOne<fakeObj>>().branded.toEqualTypeOf<AtLeastOne<optionalObj>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not accept empty objects, even if optional properties are present", () => {
|
||||||
|
expectTypeOf<Record<string, never>>().not.toExtend<AtLeastOne<fakeObj>>();
|
||||||
|
expectTypeOf<Record<string, never>>().not.toExtend<AtLeastOne<optionalObj>>();
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user