mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-06 15:39:27 +02:00
Compare commits
16 Commits
9bddbeb872
...
e81c49c969
Author | SHA1 | Date | |
---|---|---|---|
|
e81c49c969 | ||
|
3b36ab17e4 | ||
|
6866248b41 | ||
|
9298ff8282 | ||
|
7e7ca6b3fa | ||
|
cd890025d1 | ||
|
71801fe298 | ||
|
e968063eaa | ||
|
b98ff5ae90 | ||
|
c89accc673 | ||
|
df8d1dc8c7 | ||
|
9455030fbe | ||
|
dbea701d6d | ||
|
641f5f5b97 | ||
|
f5154179b3 | ||
|
49825a6729 |
@ -5912,20 +5912,21 @@ export class ProtectAttr extends AddBattlerTagAttr {
|
||||
getCondition(): MoveConditionFunc {
|
||||
return ((user, target, move): boolean => {
|
||||
let timesUsed = 0;
|
||||
const moveHistory = user.getLastXMoves();
|
||||
let turnMove: TurnMove | undefined;
|
||||
|
||||
while (moveHistory.length) {
|
||||
turnMove = moveHistory.shift();
|
||||
if (!allMoves[turnMove?.move ?? MoveId.NONE].hasAttr("ProtectAttr") || turnMove?.result !== MoveResult.SUCCESS) {
|
||||
for (const turnMove of user.getLastXMoves(-1).slice()) {
|
||||
if (
|
||||
// Quick & Wide guard increment the Protect counter without using it for fail chance
|
||||
!(allMoves[turnMove.move].hasAttr("ProtectAttr") ||
|
||||
[MoveId.QUICK_GUARD, MoveId.WIDE_GUARD].includes(turnMove.move)) ||
|
||||
turnMove.result !== MoveResult.SUCCESS
|
||||
) {
|
||||
break;
|
||||
}
|
||||
timesUsed++;
|
||||
|
||||
timesUsed++
|
||||
}
|
||||
if (timesUsed) {
|
||||
return !user.randBattleSeedInt(Math.pow(3, timesUsed));
|
||||
}
|
||||
return true;
|
||||
|
||||
return timesUsed === 0 || user.randBattleSeedInt(Math.pow(3, timesUsed)) === 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -253,7 +253,6 @@ export class PokemonTempSummonData {
|
||||
* Only currently used for positioning the battle cursor.
|
||||
*/
|
||||
turnCount = 1;
|
||||
|
||||
/**
|
||||
* The number of turns this pokemon has spent in the active position since the start of the wave
|
||||
* without switching out.
|
||||
|
@ -5094,6 +5094,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
*/
|
||||
resetWaveData(): void {
|
||||
this.waveData = new PokemonWaveData();
|
||||
this.tempSummonData.waveTurnCount = 1;
|
||||
}
|
||||
|
||||
resetTera(): void {
|
||||
|
@ -58,12 +58,6 @@ export class BattleEndPhase extends BattlePhase {
|
||||
globalScene.phaseManager.unshiftNew("GameOverPhase", true);
|
||||
}
|
||||
|
||||
for (const pokemon of globalScene.getField()) {
|
||||
if (pokemon) {
|
||||
pokemon.tempSummonData.waveTurnCount = 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (const pokemon of globalScene.getPokemonAllowedInBattle()) {
|
||||
applyAbAttrs("PostBattleAbAttr", { pokemon, victory: this.isVictory });
|
||||
}
|
||||
|
27
test/@types/test-helpers.ts
Normal file
27
test/@types/test-helpers.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import type { AtLeastOne, NonFunctionPropertiesRecursive as nonFunc } from "#types/type-helpers";
|
||||
|
||||
/**
|
||||
* Helper type to admit an object containing the given properties
|
||||
* _and at least 1 other non-function property_.
|
||||
* @example
|
||||
* ```ts
|
||||
* type foo = {
|
||||
* qux: 1 | 2 | 3,
|
||||
* bar: number,
|
||||
* baz: string
|
||||
* quux: () => void; // ignored!
|
||||
* }
|
||||
*
|
||||
* type quxAndSomethingElse = OneOther<foo, "qux">
|
||||
*
|
||||
* const good1: quxAndSomethingElse = {qux: 1, bar: 3} // OK!
|
||||
* const good2: quxAndSomethingElse = {qux: 2, baz: "4", bar: 12} // OK!
|
||||
* const bad1: quxAndSomethingElse = {baz: "4", bar: 12} // Errors because `qux` is required
|
||||
* const bad2: quxAndSomethingElse = {qux: 1} // Errors because at least 1 thing _other_ than `qux` is required
|
||||
* ```
|
||||
* @typeParam O - The object to source keys from
|
||||
* @typeParam K - One or more of O's keys to render mandatory
|
||||
*/
|
||||
export type OneOther<O extends object, K extends keyof O> = AtLeastOne<Omit<nonFunc<O>, K>> & {
|
||||
[key in K]: O[K];
|
||||
};
|
139
test/@types/vitest.d.ts
vendored
139
test/@types/vitest.d.ts
vendored
@ -1,23 +1,32 @@
|
||||
import type { TerrainType } from "#app/data/terrain";
|
||||
import type { ArenaTag } from "#data/arena-tag";
|
||||
import type { AbilityId } from "#enums/ability-id";
|
||||
import type { ArenaTagType } from "#enums/arena-tag-type";
|
||||
import type { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import type { MoveId } from "#enums/move-id";
|
||||
import type { PokemonType } from "#enums/pokemon-type";
|
||||
import type { BattleStat, EffectiveStat, Stat } from "#enums/stat";
|
||||
import type { StatusEffect } from "#enums/status-effect";
|
||||
import type { WeatherType } from "#enums/weather-type";
|
||||
import type { Arena } from "#field/arena";
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
import type { ToHaveEffectiveStatMatcherOptions } from "#test/test-utils/matchers/to-have-effective-stat";
|
||||
import type { toHaveEffectiveStatOptions } 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 { TurnMove } from "#types/turn-move";
|
||||
import type { AtLeastOne } from "#types/type-helpers";
|
||||
import type { toDmgValue } from "utils/common";
|
||||
import type { expect } from "vitest";
|
||||
import type { PositionalTag } from "#data/positional-tags/positional-tag";
|
||||
import { PositionalTagType } from "#enums/positional-tag-type";
|
||||
import type Overrides from "#app/overrides";
|
||||
import type { ArenaTagSide } from "#enums/arena-tag-side";
|
||||
import type { PokemonMove } from "#moves/pokemon-move";
|
||||
import { toHaveArenaTagOptions } from "#test/test-utils/matchers/to-have-arena-tag";
|
||||
import { toHavePositionalTagOptions } from "#test/test-utils/matchers/to-have-positional-tag";
|
||||
|
||||
declare module "vitest" {
|
||||
interface Assertion {
|
||||
interface Assertion<T> {
|
||||
/**
|
||||
* Check whether an array contains EXACTLY the given items (in any order).
|
||||
*
|
||||
@ -27,45 +36,9 @@ declare module "vitest" {
|
||||
* @param expected - The expected contents of the array, in any order
|
||||
* @see {@linkcode expect.arrayContaining}
|
||||
*/
|
||||
toEqualArrayUnsorted<E>(expected: E[]): void;
|
||||
toEqualArrayUnsorted(expected: T[]): void;
|
||||
|
||||
/**
|
||||
* Check whether a {@linkcode Pokemon}'s current typing includes the given types.
|
||||
*
|
||||
* @param expected - The expected types (in any order)
|
||||
* @param options - The options passed to the matcher
|
||||
*/
|
||||
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;
|
||||
// #region Arena Matchers
|
||||
|
||||
/**
|
||||
* Check whether the current {@linkcode WeatherType} is as expected.
|
||||
@ -80,9 +53,63 @@ declare module "vitest" {
|
||||
toHaveTerrain(expectedTerrainType: TerrainType): void;
|
||||
|
||||
/**
|
||||
* Check whether a {@linkcode Pokemon} is at full HP.
|
||||
* Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}.
|
||||
* @param expectedTag - A partially-filled {@linkcode ArenaTag} containing the desired properties
|
||||
*/
|
||||
toHaveFullHp(): void;
|
||||
toHaveArenaTag<A extends ArenaTagType>(expectedTag: toHaveArenaTagOptions<A>): void;
|
||||
/**
|
||||
* Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}.
|
||||
* @param expectedType - The {@linkcode ArenaTagType} of the desired tag
|
||||
* @param side - The {@linkcode ArenaTagSide | side of the field} the tag should affect, or
|
||||
* {@linkcode ArenaTagSide.BOTH} to check both sides;
|
||||
* default `ArenaTagSide.BOTH`
|
||||
*/
|
||||
toHaveArenaTag(expectedType: ArenaTagType, side?: ArenaTagSide): void;
|
||||
|
||||
/**
|
||||
* Check whether the current {@linkcode Arena} contains the given {@linkcode PositionalTag}.
|
||||
* @param expectedTag - A partially-filled {@linkcode PositionalTag} containing the desired properties
|
||||
*/
|
||||
toHavePositionalTag<P extends PositionalTagType>(expectedTag: toHavePositionalTagOptions<P>): void;
|
||||
/**
|
||||
* Check whether the current {@linkcode Arena} contains the given number of {@linkcode PositionalTag}s.
|
||||
* @param expectedType - The {@linkcode PositionalTagType} of the desired tag
|
||||
* @param count - The number of instances of {@linkcode expectedType} that should be active;
|
||||
* defaults to `1` and must be within the range `[0, 4]`
|
||||
*/
|
||||
toHavePositionalTag(expectedType: PositionalTagType, count?: number): void;
|
||||
|
||||
// #endregion Arena Matchers
|
||||
|
||||
// #region Pokemon Matchers
|
||||
|
||||
/**
|
||||
* Check whether a {@linkcode Pokemon}'s current typing includes the given types.
|
||||
* @param expectedTags - The expected {@linkcode PokemonType}s to check against; must have length `>0`
|
||||
* @param options - The {@linkcode toHaveTypesOptions | options} passed to the matcher
|
||||
*/
|
||||
toHaveTypes(expectedTags: PokemonType[], options?: toHaveTypesOptions): void;
|
||||
|
||||
/**
|
||||
* Check whether a {@linkcode Pokemon} has used a move matching the given criteria.
|
||||
* @param expectedMove - 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)
|
||||
* @see {@linkcode Pokemon.getLastXMoves}
|
||||
*/
|
||||
toHaveUsedMove(expectedMove: 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 - The {@linkcode toHaveEffectiveStatOptions | options} passed to the matcher
|
||||
* @remarks
|
||||
* If you want to check the stat **before** modifiers are applied, use {@linkcode Pokemon.getStat} instead.
|
||||
*/
|
||||
toHaveEffectiveStat(stat: EffectiveStat, expectedValue: number, options?: toHaveEffectiveStatOptions): void;
|
||||
|
||||
/**
|
||||
* Check whether a {@linkcode Pokemon} has a specific {@linkcode StatusEffect | non-volatile status effect}.
|
||||
@ -106,7 +133,7 @@ declare module "vitest" {
|
||||
|
||||
/**
|
||||
* Check whether a {@linkcode Pokemon} has applied a specific {@linkcode AbilityId}.
|
||||
* @param expectedAbilityId - The expected {@linkcode AbilityId}
|
||||
* @param expectedAbilityId - The expected {@linkcode AbilityId} to check for
|
||||
*/
|
||||
toHaveAbilityApplied(expectedAbilityId: AbilityId): void;
|
||||
|
||||
@ -116,24 +143,36 @@ declare module "vitest" {
|
||||
*/
|
||||
toHaveHp(expectedHp: number): 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 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`.
|
||||
* When checking whether an enemy wild Pokemon is fainted, one must store a reference to it in a variable _before_ the fainting effect occurs,
|
||||
* as otherwise the Pokemon will be removed from the field and garbage collected.
|
||||
*/
|
||||
toHaveFainted(): void;
|
||||
|
||||
/**
|
||||
* Check whether a {@linkcode Pokemon} is at full HP.
|
||||
*/
|
||||
toHaveFullHp(): 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 moveId - 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.
|
||||
* If the Pokemon's moveset has been set via {@linkcode Overrides.MOVESET_OVERRIDE}/{@linkcode Overrides.OPP_MOVESET_OVERRIDE}
|
||||
* or does not contain exactly 1 copy of {@linkcode moveId}, this will fail the test.
|
||||
*/
|
||||
toHaveUsedPP(expectedMove: MoveId, ppUsed: number | "all"): void;
|
||||
toHaveUsedPP(moveId: MoveId, ppUsed: number | "all"): void;
|
||||
|
||||
// #region Pokemon Matchers
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { toEqualArrayUnsorted } from "#test/test-utils/matchers/to-equal-array-unsorted";
|
||||
import { toHaveAbilityApplied } from "#test/test-utils/matchers/to-have-ability-applied";
|
||||
import { toHaveArenaTag } from "#test/test-utils/matchers/to-have-arena-tag";
|
||||
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 { toHavePositionalTag } from "#test/test-utils/matchers/to-have-positional-tag";
|
||||
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";
|
||||
@ -22,18 +24,20 @@ import { expect } from "vitest";
|
||||
|
||||
expect.extend({
|
||||
toEqualArrayUnsorted,
|
||||
toHaveWeather,
|
||||
toHaveTerrain,
|
||||
toHaveArenaTag,
|
||||
toHavePositionalTag,
|
||||
toHaveTypes,
|
||||
toHaveUsedMove,
|
||||
toHaveEffectiveStat,
|
||||
toHaveTakenDamage,
|
||||
toHaveWeather,
|
||||
toHaveTerrain,
|
||||
toHaveFullHp,
|
||||
toHaveStatusEffect,
|
||||
toHaveStatStage,
|
||||
toHaveBattlerTag,
|
||||
toHaveAbilityApplied,
|
||||
toHaveHp,
|
||||
toHaveTakenDamage,
|
||||
toHaveFullHp,
|
||||
toHaveFainted,
|
||||
toHaveUsedPP,
|
||||
});
|
||||
|
@ -5,7 +5,7 @@ import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Moves - Baneful Bunker", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -26,55 +26,51 @@ describe("Moves - Baneful Bunker", () => {
|
||||
|
||||
game.override
|
||||
.battleStyle("single")
|
||||
.moveset(MoveId.SLASH)
|
||||
.enemySpecies(SpeciesId.SNORLAX)
|
||||
.moveset([MoveId.SLASH, MoveId.FLASH_CANNON])
|
||||
.enemySpecies(SpeciesId.TOXAPEX)
|
||||
.enemyAbility(AbilityId.INSOMNIA)
|
||||
.enemyMoveset(MoveId.BANEFUL_BUNKER)
|
||||
.startingLevel(100)
|
||||
.enemyLevel(100);
|
||||
});
|
||||
test("should protect the user and poison attackers that make contact", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.field.getPlayerPokemon();
|
||||
const enemyPokemon = game.field.getEnemyPokemon();
|
||||
function expectProtected() {
|
||||
expect(game.scene.getEnemyPokemon()?.hp).toBe(game.scene.getEnemyPokemon()?.getMaxHp());
|
||||
expect(game.scene.getPlayerPokemon()?.status?.effect).toBe(StatusEffect.POISON);
|
||||
}
|
||||
|
||||
it("should protect the user and poison attackers that make contact", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
|
||||
|
||||
game.move.select(MoveId.SLASH);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
||||
expect(leadPokemon.status?.effect === StatusEffect.POISON).toBeTruthy();
|
||||
|
||||
expectProtected();
|
||||
});
|
||||
test("should protect the user and poison attackers that make contact, regardless of accuracy checks", async () => {
|
||||
|
||||
it("should ignore accuracy checks", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.field.getPlayerPokemon();
|
||||
const enemyPokemon = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.select(MoveId.SLASH);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
await game.phaseInterceptor.to("MoveEndPhase"); // baneful bunker
|
||||
await game.move.forceMiss();
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
||||
expect(leadPokemon.status?.effect === StatusEffect.POISON).toBeTruthy();
|
||||
|
||||
expectProtected();
|
||||
});
|
||||
|
||||
test("should not poison attackers that don't make contact", async () => {
|
||||
game.override.moveset(MoveId.FLASH_CANNON);
|
||||
it("should block non-contact moves without poisoning attackers", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.field.getPlayerPokemon();
|
||||
const enemyPokemon = game.field.getEnemyPokemon();
|
||||
const charizard = game.field.getPlayerPokemon();
|
||||
const toxapex = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.select(MoveId.FLASH_CANNON);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
await game.move.forceMiss();
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
||||
expect(leadPokemon.status?.effect === StatusEffect.POISON).toBeFalsy();
|
||||
|
||||
expect(toxapex.hp).toBe(toxapex.getMaxHp());
|
||||
expect(charizard.status?.effect).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
@ -1,12 +1,14 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { BerryPhase } from "#phases/berry-phase";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Moves - Crafty Shield", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -27,68 +29,100 @@ describe("Moves - Crafty Shield", () => {
|
||||
|
||||
game.override
|
||||
.battleStyle("double")
|
||||
.moveset([MoveId.CRAFTY_SHIELD, MoveId.SPLASH, MoveId.SWORDS_DANCE])
|
||||
.enemySpecies(SpeciesId.SNORLAX)
|
||||
.enemyMoveset([MoveId.GROWL])
|
||||
.enemySpecies(SpeciesId.DUSKNOIR)
|
||||
.enemyMoveset(MoveId.GROWL)
|
||||
.enemyAbility(AbilityId.INSOMNIA)
|
||||
.startingLevel(100)
|
||||
.enemyLevel(100);
|
||||
});
|
||||
|
||||
test("should protect the user and allies from status moves", async () => {
|
||||
it("should protect the user and allies from status moves", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD, SpeciesId.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerField();
|
||||
const [charizard, blastoise] = game.scene.getPlayerField();
|
||||
game.move.use(MoveId.CRAFTY_SHIELD, BattlerIndex.PLAYER);
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.move.forceEnemyMove(MoveId.GROWL);
|
||||
await game.move.forceEnemyMove(MoveId.GROWL);
|
||||
|
||||
game.move.select(MoveId.CRAFTY_SHIELD);
|
||||
game.move.select(MoveId.SPLASH, 1);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
leadPokemon.forEach(p => expect(p.getStatStage(Stat.ATK)).toBe(0));
|
||||
expect(charizard.getStatStage(Stat.ATK)).toBe(0);
|
||||
expect(blastoise.getStatStage(Stat.ATK)).toBe(0);
|
||||
});
|
||||
|
||||
test("should not protect the user and allies from attack moves", async () => {
|
||||
game.override.enemyMoveset([MoveId.TACKLE]);
|
||||
|
||||
it("should not protect the user and allies from attack moves", async () => {
|
||||
game.override.enemyMoveset(MoveId.TACKLE);
|
||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD, SpeciesId.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerField();
|
||||
const [charizard, blastoise] = game.scene.getPlayerField();
|
||||
|
||||
game.move.select(MoveId.CRAFTY_SHIELD);
|
||||
game.move.select(MoveId.SPLASH, 1);
|
||||
game.move.use(MoveId.CRAFTY_SHIELD, BattlerIndex.PLAYER);
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.move.forceEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER);
|
||||
await game.move.forceEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER_2);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
expect(leadPokemon.some(p => p.hp < p.getMaxHp())).toBeTruthy();
|
||||
expect(charizard.isFullHp()).toBe(false);
|
||||
expect(blastoise.isFullHp()).toBe(false);
|
||||
});
|
||||
|
||||
test("should protect the user and allies from moves that ignore other protection", async () => {
|
||||
game.override.enemySpecies(SpeciesId.DUSCLOPS).enemyMoveset([MoveId.CURSE]);
|
||||
|
||||
it("should not block entry hazards and field-targeted moves", async () => {
|
||||
game.override.enemyMoveset([MoveId.PERISH_SONG, MoveId.TOXIC_SPIKES]);
|
||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD, SpeciesId.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerField();
|
||||
const [charizard, blastoise] = game.scene.getPlayerField();
|
||||
|
||||
game.move.select(MoveId.CRAFTY_SHIELD);
|
||||
game.move.select(MoveId.SPLASH, 1);
|
||||
game.move.use(MoveId.CRAFTY_SHIELD, BattlerIndex.PLAYER);
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.move.forceEnemyMove(MoveId.PERISH_SONG);
|
||||
await game.move.forceEnemyMove(MoveId.TOXIC_SPIKES);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
leadPokemon.forEach(p => expect(p.getTag(BattlerTagType.CURSED)).toBeUndefined());
|
||||
expect(game.scene.arena.getTagOnSide(ArenaTagType.TOXIC_SPIKES, ArenaTagSide.PLAYER)).toBeDefined();
|
||||
expect(charizard.getTag(BattlerTagType.PERISH_SONG)).toBeDefined();
|
||||
expect(blastoise.getTag(BattlerTagType.PERISH_SONG)).toBeDefined();
|
||||
});
|
||||
|
||||
test("should not block allies' self-targeted moves", async () => {
|
||||
it("should protect the user and allies from moves that ignore other protection", async () => {
|
||||
game.override.moveset(MoveId.CURSE);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD, SpeciesId.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerField();
|
||||
const [charizard, blastoise] = game.scene.getPlayerField();
|
||||
|
||||
game.move.select(MoveId.CRAFTY_SHIELD);
|
||||
game.move.select(MoveId.SWORDS_DANCE, 1);
|
||||
game.move.use(MoveId.CRAFTY_SHIELD, BattlerIndex.PLAYER);
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.move.forceEnemyMove(MoveId.CURSE, BattlerIndex.PLAYER);
|
||||
await game.move.forceEnemyMove(MoveId.CURSE, BattlerIndex.PLAYER_2);
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(leadPokemon[0].getStatStage(Stat.ATK)).toBe(0);
|
||||
expect(leadPokemon[1].getStatStage(Stat.ATK)).toBe(2);
|
||||
expect(charizard.getTag(BattlerTagType.CURSED)).toBeUndefined();
|
||||
expect(blastoise.getTag(BattlerTagType.CURSED)).toBeUndefined();
|
||||
|
||||
const [dusknoir1, dusknoir2] = game.scene.getEnemyField();
|
||||
expect(dusknoir1).toHaveFullHp();
|
||||
expect(dusknoir2).toHaveFullHp();
|
||||
});
|
||||
|
||||
it("should not block allies' self or ally-targeted moves", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD, SpeciesId.BLASTOISE]);
|
||||
|
||||
const [charizard, blastoise] = game.scene.getPlayerField();
|
||||
|
||||
game.move.use(MoveId.CRAFTY_SHIELD, BattlerIndex.PLAYER);
|
||||
game.move.use(MoveId.SWORDS_DANCE, BattlerIndex.PLAYER_2);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(charizard.getStatStage(Stat.ATK)).toBe(0);
|
||||
expect(blastoise.getStatStage(Stat.ATK)).toBe(2);
|
||||
|
||||
game.move.use(MoveId.HOWL, BattlerIndex.PLAYER);
|
||||
game.move.use(MoveId.CRAFTY_SHIELD, BattlerIndex.PLAYER_2);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(charizard.getStatStage(Stat.ATK)).toBe(1);
|
||||
expect(blastoise.getStatStage(Stat.ATK)).toBe(3);
|
||||
});
|
||||
});
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { HitResult } from "#enums/hit-result";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
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";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("Moves - Endure", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -22,7 +23,7 @@ describe("Moves - Endure", () => {
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.moveset([MoveId.THUNDER, MoveId.BULLET_SEED, MoveId.TOXIC, MoveId.SHEER_COLD])
|
||||
.moveset([MoveId.THUNDER, MoveId.BULLET_SEED, MoveId.SHEER_COLD])
|
||||
.ability(AbilityId.SKILL_LINK)
|
||||
.startingLevel(100)
|
||||
.battleStyle("single")
|
||||
@ -32,7 +33,7 @@ describe("Moves - Endure", () => {
|
||||
.enemyMoveset(MoveId.ENDURE);
|
||||
});
|
||||
|
||||
it("should let the pokemon survive with 1 HP", async () => {
|
||||
it("should let the pokemon survive with 1 HP from attacks", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.ARCEUS]);
|
||||
|
||||
game.move.select(MoveId.THUNDER);
|
||||
@ -41,7 +42,7 @@ describe("Moves - Endure", () => {
|
||||
expect(game.field.getEnemyPokemon().hp).toBe(1);
|
||||
});
|
||||
|
||||
it("should let the pokemon survive with 1 HP when hit with a multihit move", async () => {
|
||||
it("should let the pokemon survive with 1 HP from multi-strike moves", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.ARCEUS]);
|
||||
|
||||
game.move.select(MoveId.BULLET_SEED);
|
||||
@ -57,30 +58,27 @@ describe("Moves - Endure", () => {
|
||||
game.move.select(MoveId.SHEER_COLD);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(enemy.isFainted()).toBeFalsy();
|
||||
expect(enemy.hp).toBe(1);
|
||||
});
|
||||
|
||||
// comprehensive indirect damage test copied from Reviver Seed test
|
||||
it.each([
|
||||
{ moveType: "Damaging Move Chip Damage", move: MoveId.SALT_CURE },
|
||||
{ moveType: "Chip Damage", move: MoveId.LEECH_SEED },
|
||||
{ moveType: "Trapping Chip Damage", move: MoveId.WHIRLPOOL },
|
||||
{ moveType: "Status Effect Damage", move: MoveId.TOXIC },
|
||||
{ moveType: "Damaging Move Chip", move: MoveId.SALT_CURE },
|
||||
{ moveType: "Status Move Chip", move: MoveId.LEECH_SEED },
|
||||
{ moveType: "Partial Trapping move", move: MoveId.WHIRLPOOL },
|
||||
{ moveType: "Status Effect", move: MoveId.TOXIC },
|
||||
{ moveType: "Weather", move: MoveId.SANDSTORM },
|
||||
])("should not prevent fainting from $moveType", async ({ move }) => {
|
||||
game.override
|
||||
.enemyLevel(1)
|
||||
.startingLevel(100)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.moveset(move)
|
||||
.enemyMoveset(MoveId.ENDURE);
|
||||
])("should not prevent fainting from $moveType Damage", async ({ move }) => {
|
||||
game.override.moveset(move).enemyLevel(100);
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.FEEBAS]);
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
enemy.damageAndUpdate(enemy.hp - 1);
|
||||
enemy.hp = 2;
|
||||
// force attack to do 1 dmg (for salt cure)
|
||||
vi.spyOn(enemy, "getAttackDamage").mockReturnValue({ cancelled: false, result: HitResult.EFFECTIVE, damage: 1 });
|
||||
|
||||
game.move.select(move);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(enemy.isFainted()).toBeTruthy();
|
||||
expect(enemy.isFainted()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
@ -1,15 +1,14 @@
|
||||
import { ArenaTrapTag } from "#data/arena-tag";
|
||||
import { allMoves } from "#data/data-lists";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
||||
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 { Stat } from "#enums/stat";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("Moves - Protect", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -27,90 +26,210 @@ describe("Moves - Protect", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
|
||||
game.override
|
||||
.battleStyle("single")
|
||||
.moveset([MoveId.PROTECT])
|
||||
.moveset([MoveId.PROTECT, MoveId.SPIKY_SHIELD, MoveId.ENDURE, MoveId.SPLASH])
|
||||
.enemySpecies(SpeciesId.SNORLAX)
|
||||
.enemyAbility(AbilityId.INSOMNIA)
|
||||
.enemyMoveset([MoveId.TACKLE])
|
||||
.enemyMoveset(MoveId.LUMINA_CRASH)
|
||||
.startingLevel(100)
|
||||
.enemyLevel(100);
|
||||
});
|
||||
|
||||
test("should protect the user from attacks", async () => {
|
||||
it("should protect the user from attacks and their secondary effects", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.field.getPlayerPokemon();
|
||||
const charizard = game.field.getPlayerPokemon();
|
||||
|
||||
game.move.select(MoveId.PROTECT);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
|
||||
expect(charizard.hp).toBe(charizard.getMaxHp());
|
||||
expect(charizard.getStatStage(Stat.SPDEF)).toBe(0);
|
||||
expect(charizard);
|
||||
});
|
||||
|
||||
test("should prevent secondary effects from the opponent's attack", async () => {
|
||||
game.override.enemyMoveset([MoveId.CEASELESS_EDGE]);
|
||||
vi.spyOn(allMoves[MoveId.CEASELESS_EDGE], "accuracy", "get").mockReturnValue(100);
|
||||
|
||||
it.each<{ numTurns: number; chance: number }>([
|
||||
{ numTurns: 1, chance: 3 },
|
||||
{ numTurns: 2, chance: 9 },
|
||||
{ numTurns: 3, chance: 27 },
|
||||
{ numTurns: 4, chance: 81 },
|
||||
])("should have a 1/$chance success rate after $numTurns successful uses", async ({ numTurns, chance }) => {
|
||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.field.getPlayerPokemon();
|
||||
const charizard = game.scene.getPlayerPokemon()!;
|
||||
|
||||
// mock RNG roll to suceed unless exactly the desired chance is hit
|
||||
vi.spyOn(charizard, "randBattleSeedInt").mockImplementation(range => (range !== chance ? 0 : 1));
|
||||
const conditionSpy = vi.spyOn(allMoves[MoveId.PROTECT]["conditions"][0], "apply");
|
||||
|
||||
// click protect many times
|
||||
for (let x = 0; x < numTurns; x++) {
|
||||
game.move.select(MoveId.PROTECT);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(charizard.hp).toBe(charizard.getMaxHp());
|
||||
expect(charizard.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
expect(conditionSpy).toHaveLastReturnedWith(true);
|
||||
}
|
||||
|
||||
game.move.select(MoveId.PROTECT);
|
||||
await game.toNextTurn();
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
|
||||
expect(game.scene.arena.getTagOnSide(ArenaTrapTag, ArenaTagSide.ENEMY)).toBeUndefined();
|
||||
expect(charizard.hp).toBeLessThan(charizard.getMaxHp());
|
||||
expect(charizard.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
expect(conditionSpy).toHaveLastReturnedWith(false);
|
||||
});
|
||||
|
||||
test("should protect the user from status moves", async () => {
|
||||
game.override.enemyMoveset([MoveId.CHARM]);
|
||||
|
||||
it("should share fail chance with all move variants", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.field.getPlayerPokemon();
|
||||
const charizard = game.field.getPlayerPokemon();
|
||||
charizard.summonData.moveHistory = [
|
||||
{ move: MoveId.ENDURE, result: MoveResult.SUCCESS, targets: [BattlerIndex.PLAYER], useMode: MoveUseMode.NORMAL },
|
||||
{
|
||||
move: MoveId.SPIKY_SHIELD,
|
||||
result: MoveResult.SUCCESS,
|
||||
targets: [BattlerIndex.PLAYER],
|
||||
useMode: MoveUseMode.NORMAL,
|
||||
},
|
||||
];
|
||||
// force protect to fail on anything >=2 uses (1/9 chance)
|
||||
vi.spyOn(charizard, "randBattleSeedInt").mockImplementation(range => (range >= 9 ? 1 : 0));
|
||||
|
||||
game.move.select(MoveId.PROTECT);
|
||||
await game.toNextTurn();
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(leadPokemon.getStatStage(Stat.ATK)).toBe(0);
|
||||
expect(charizard.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
});
|
||||
|
||||
test("should stop subsequent hits of a multi-hit move", async () => {
|
||||
it("should reset fail chance on move failure", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
|
||||
|
||||
const charizard = game.scene.getPlayerPokemon()!;
|
||||
// force protect to always fail if RNG roll attempt is made
|
||||
vi.spyOn(charizard, "randBattleSeedInt").mockReturnValue(1);
|
||||
|
||||
game.move.select(MoveId.PROTECT);
|
||||
await game.toNextTurn();
|
||||
expect(charizard.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
|
||||
game.move.select(MoveId.SPIKY_SHIELD);
|
||||
await game.toNextTurn();
|
||||
expect(charizard.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
|
||||
game.move.select(MoveId.SPIKY_SHIELD);
|
||||
await game.toNextTurn();
|
||||
expect(charizard.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
});
|
||||
|
||||
it("should reset fail chance on using another move", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
|
||||
|
||||
const charizard = game.scene.getPlayerPokemon()!;
|
||||
// force protect to always fail if RNG roll attempt is made
|
||||
vi.spyOn(charizard, "randBattleSeedInt").mockReturnValue(1);
|
||||
|
||||
game.move.select(MoveId.PROTECT);
|
||||
await game.toNextTurn();
|
||||
expect(charizard.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(MoveId.PROTECT);
|
||||
await game.toNextTurn();
|
||||
expect(charizard.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
});
|
||||
|
||||
it("should reset fail chance on starting a new wave", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
|
||||
|
||||
const charizard = game.field.getPlayerPokemon();
|
||||
// force protect to always fail if RNG roll attempt is made
|
||||
vi.spyOn(charizard, "randBattleSeedInt").mockReturnValue(1);
|
||||
|
||||
game.move.select(MoveId.PROTECT);
|
||||
// Wait until move end phase to kill opponent to ensure protect doesn't fail due to going last
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
await game.doKillOpponents();
|
||||
await game.toNextWave();
|
||||
expect(charizard.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
|
||||
game.move.select(MoveId.SPIKY_SHIELD);
|
||||
expect(charizard.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
});
|
||||
|
||||
it("should not be blocked by Psychic Terrain", async () => {
|
||||
game.override.ability(AbilityId.PSYCHIC_SURGE);
|
||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
|
||||
|
||||
const charizard = game.scene.getPlayerPokemon()!;
|
||||
game.move.select(MoveId.PROTECT);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(charizard.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
});
|
||||
|
||||
it("should stop subsequent hits of multi-hit moves", async () => {
|
||||
game.override.enemyMoveset([MoveId.TACHYON_CUTTER]);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.field.getPlayerPokemon();
|
||||
const charizard = game.field.getPlayerPokemon();
|
||||
const enemyPokemon = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.select(MoveId.PROTECT);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
|
||||
expect(charizard.hp).toBe(charizard.getMaxHp());
|
||||
expect(enemyPokemon.turnData.hitCount).toBe(1);
|
||||
});
|
||||
|
||||
test("should fail if the user is the last to move in the turn", async () => {
|
||||
game.override.enemyMoveset([MoveId.PROTECT]);
|
||||
|
||||
it("should fail if the user moves last in the turn", async () => {
|
||||
game.override.enemyMoveset(MoveId.PROTECT);
|
||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.field.getPlayerPokemon();
|
||||
const charizard = game.field.getPlayerPokemon();
|
||||
const enemyPokemon = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.select(MoveId.PROTECT);
|
||||
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
expect(leadPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
expect(charizard.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
});
|
||||
|
||||
it("should not block Protection-bypassing moves or Future Sight", async () => {
|
||||
game.override.enemyMoveset([MoveId.FUTURE_SIGHT, MoveId.MIGHTY_CLEAVE, MoveId.SPORE]);
|
||||
await game.classicMode.startBattle([SpeciesId.AGGRON]);
|
||||
|
||||
const aggron = game.scene.getPlayerPokemon()!;
|
||||
vi.spyOn(aggron, "randBattleSeedInt").mockReturnValue(0);
|
||||
|
||||
// Turn 1: setup future sight
|
||||
game.move.select(MoveId.PROTECT);
|
||||
await game.move.forceEnemyMove(MoveId.FUTURE_SIGHT);
|
||||
await game.toNextTurn();
|
||||
|
||||
// Turn 2: mighty cleave
|
||||
game.move.select(MoveId.PROTECT);
|
||||
await game.move.forceEnemyMove(MoveId.MIGHTY_CLEAVE);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(aggron.hp).toBeLessThan(aggron.getMaxHp());
|
||||
|
||||
aggron.hp = aggron.getMaxHp();
|
||||
|
||||
// turn 3: Future Sight hits
|
||||
game.move.select(MoveId.PROTECT);
|
||||
await game.move.forceEnemyMove(MoveId.SPORE);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(aggron.hp).toBeLessThan(aggron.getMaxHp());
|
||||
expect(aggron.status?.effect).toBeUndefined(); // check that protect actually worked
|
||||
});
|
||||
|
||||
// TODO: Add test
|
||||
it.todo("should not reset counter when throwing balls");
|
||||
});
|
||||
|
@ -3,10 +3,9 @@ import { BattlerIndex } from "#enums/battler-index";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("Moves - Quick Guard", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -27,74 +26,72 @@ describe("Moves - Quick Guard", () => {
|
||||
|
||||
game.override
|
||||
.battleStyle("double")
|
||||
.moveset([MoveId.QUICK_GUARD, MoveId.SPLASH, MoveId.FOLLOW_ME])
|
||||
.moveset([MoveId.QUICK_GUARD, MoveId.SPLASH, MoveId.SPIKY_SHIELD])
|
||||
.enemySpecies(SpeciesId.SNORLAX)
|
||||
.enemyMoveset([MoveId.QUICK_ATTACK])
|
||||
.enemyAbility(AbilityId.INSOMNIA)
|
||||
.enemyMoveset(MoveId.QUICK_ATTACK)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.startingLevel(100)
|
||||
.enemyLevel(100);
|
||||
});
|
||||
|
||||
test("should protect the user and allies from priority moves", async () => {
|
||||
it("should protect the user and allies from priority moves", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD, SpeciesId.BLASTOISE]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerField();
|
||||
|
||||
game.move.select(MoveId.QUICK_GUARD);
|
||||
game.move.select(MoveId.SPLASH, 1);
|
||||
const [charizard, blastoise] = game.scene.getPlayerField();
|
||||
|
||||
game.move.select(MoveId.QUICK_GUARD, BattlerIndex.PLAYER);
|
||||
game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.move.forceEnemyMove(MoveId.QUICK_ATTACK, BattlerIndex.PLAYER);
|
||||
await game.move.forceEnemyMove(MoveId.QUICK_ATTACK, BattlerIndex.PLAYER_2);
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
playerPokemon.forEach(p => expect(p.hp).toBe(p.getMaxHp()));
|
||||
expect(charizard.hp).toBe(charizard.getMaxHp());
|
||||
expect(blastoise.hp).toBe(blastoise.getMaxHp());
|
||||
});
|
||||
|
||||
test("should protect the user and allies from Prankster-boosted moves", async () => {
|
||||
game.override.enemyAbility(AbilityId.PRANKSTER).enemyMoveset([MoveId.GROWL]);
|
||||
|
||||
it.each<{ name: string; move: MoveId; ability: AbilityId }>([
|
||||
{ name: "Prankster", move: MoveId.SPORE, ability: AbilityId.PRANKSTER },
|
||||
{ name: "Gale Wings", move: MoveId.BRAVE_BIRD, ability: AbilityId.GALE_WINGS },
|
||||
])("should protect the user and allies from $name-boosted moves", async ({ move, ability }) => {
|
||||
game.override.enemyMoveset(move).enemyAbility(ability);
|
||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD, SpeciesId.BLASTOISE]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerField();
|
||||
|
||||
game.move.select(MoveId.QUICK_GUARD);
|
||||
game.move.select(MoveId.SPLASH, 1);
|
||||
const [charizard, blastoise] = game.scene.getPlayerField();
|
||||
|
||||
game.move.select(MoveId.QUICK_GUARD, BattlerIndex.PLAYER);
|
||||
game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.move.forceEnemyMove(move, BattlerIndex.PLAYER);
|
||||
await game.move.forceEnemyMove(move, BattlerIndex.PLAYER_2);
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
playerPokemon.forEach(p => expect(p.getStatStage(Stat.ATK)).toBe(0));
|
||||
expect(charizard.hp).toBe(charizard.getMaxHp());
|
||||
expect(blastoise.hp).toBe(blastoise.getMaxHp());
|
||||
expect(charizard.status?.effect).toBeUndefined();
|
||||
expect(blastoise.status?.effect).toBeUndefined();
|
||||
});
|
||||
|
||||
test("should stop subsequent hits of a multi-hit priority move", async () => {
|
||||
game.override.enemyMoveset([MoveId.WATER_SHURIKEN]);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD, SpeciesId.BLASTOISE]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerField();
|
||||
const enemyPokemon = game.scene.getEnemyField();
|
||||
|
||||
game.move.select(MoveId.QUICK_GUARD);
|
||||
game.move.select(MoveId.FOLLOW_ME, 1);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
playerPokemon.forEach(p => expect(p.hp).toBe(p.getMaxHp()));
|
||||
enemyPokemon.forEach(p => expect(p.turnData.hitCount).toBe(1));
|
||||
});
|
||||
|
||||
test("should fail if the user is the last to move in the turn", async () => {
|
||||
game.override.battleStyle("single").enemyMoveset([MoveId.QUICK_GUARD]);
|
||||
|
||||
it("should increment (but not respect) other protection moves' fail counters", async () => {
|
||||
game.override.battleStyle("single");
|
||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
|
||||
|
||||
const playerPokemon = game.field.getPlayerPokemon();
|
||||
const enemyPokemon = game.field.getEnemyPokemon();
|
||||
const charizard = game.scene.getPlayerPokemon()!;
|
||||
// force protect to fail on anything >0 uses
|
||||
vi.spyOn(charizard, "randBattleSeedInt").mockReturnValue(1);
|
||||
|
||||
game.move.select(MoveId.QUICK_GUARD);
|
||||
await game.toNextTurn();
|
||||
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
expect(charizard.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
game.move.select(MoveId.QUICK_GUARD);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
// ignored fail chance
|
||||
expect(charizard.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
|
||||
game.move.select(MoveId.SPIKY_SHIELD);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(charizard.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
});
|
||||
});
|
||||
|
@ -1,11 +1,12 @@
|
||||
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 { SpeciesId } from "#enums/species-id";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { BerryPhase } from "#phases/berry-phase";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("Moves - Wide Guard", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -26,71 +27,84 @@ describe("Moves - Wide Guard", () => {
|
||||
|
||||
game.override
|
||||
.battleStyle("double")
|
||||
.moveset([MoveId.WIDE_GUARD, MoveId.SPLASH, MoveId.SURF])
|
||||
.moveset([MoveId.WIDE_GUARD, MoveId.SPLASH, MoveId.SURF, MoveId.SPIKY_SHIELD])
|
||||
.enemySpecies(SpeciesId.SNORLAX)
|
||||
.enemyMoveset(MoveId.SWIFT)
|
||||
.enemyMoveset([MoveId.SWIFT, MoveId.GROWL, MoveId.TACKLE])
|
||||
.enemyAbility(AbilityId.INSOMNIA)
|
||||
.startingLevel(100)
|
||||
.enemyLevel(100);
|
||||
});
|
||||
|
||||
test("should protect the user and allies from multi-target attack moves", async () => {
|
||||
it("should protect the user and allies from multi-target attack and status moves", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD, SpeciesId.BLASTOISE]);
|
||||
const [charizard, blastoise] = game.scene.getPlayerField();
|
||||
|
||||
const leadPokemon = game.scene.getPlayerField();
|
||||
game.move.select(MoveId.WIDE_GUARD, BattlerIndex.PLAYER);
|
||||
game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.move.forceEnemyMove(MoveId.SWIFT);
|
||||
await game.move.forceEnemyMove(MoveId.GROWL);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
game.move.select(MoveId.WIDE_GUARD);
|
||||
game.move.select(MoveId.SPLASH, 1);
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
leadPokemon.forEach(p => expect(p.hp).toBe(p.getMaxHp()));
|
||||
expect(charizard.hp).toBe(charizard.getMaxHp());
|
||||
expect(blastoise.hp).toBe(blastoise.getMaxHp());
|
||||
expect(charizard.getStatStage(Stat.ATK)).toBe(0);
|
||||
expect(blastoise.getStatStage(Stat.ATK)).toBe(0);
|
||||
});
|
||||
|
||||
test("should protect the user and allies from multi-target status moves", async () => {
|
||||
game.override.enemyMoveset([MoveId.GROWL]);
|
||||
|
||||
it("should not protect the user and allies from single-target moves", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD, SpeciesId.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerField();
|
||||
const [charizard, blastoise] = game.scene.getPlayerField();
|
||||
game.move.select(MoveId.WIDE_GUARD, BattlerIndex.PLAYER);
|
||||
game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.move.forceEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER);
|
||||
await game.move.forceEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER_2);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
game.move.select(MoveId.WIDE_GUARD);
|
||||
game.move.select(MoveId.SPLASH, 1);
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
leadPokemon.forEach(p => expect(p.getStatStage(Stat.ATK)).toBe(0));
|
||||
expect(charizard.hp).toBeLessThan(charizard.getMaxHp());
|
||||
expect(blastoise.hp).toBeLessThan(blastoise.getMaxHp());
|
||||
});
|
||||
|
||||
test("should not protect the user and allies from single-target moves", async () => {
|
||||
game.override.enemyMoveset([MoveId.TACKLE]);
|
||||
it("should protect the user from its ally's multi-target move", async () => {
|
||||
game.override.enemyMoveset(MoveId.SPLASH);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD, SpeciesId.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerField();
|
||||
const charizard = game.scene.getPlayerPokemon()!;
|
||||
const [snorlax1, snorlax2] = game.scene.getEnemyField();
|
||||
|
||||
game.move.select(MoveId.WIDE_GUARD);
|
||||
game.move.select(MoveId.SPLASH, 1);
|
||||
game.move.select(MoveId.WIDE_GUARD, BattlerIndex.PLAYER);
|
||||
game.move.select(MoveId.SURF, BattlerIndex.PLAYER_2);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
expect(leadPokemon.some(p => p.hp < p.getMaxHp())).toBeTruthy();
|
||||
expect(charizard.hp).toBe(charizard.getMaxHp());
|
||||
expect(snorlax1.hp).toBeLessThan(snorlax1.getMaxHp());
|
||||
expect(snorlax2.hp).toBeLessThan(snorlax2.getMaxHp());
|
||||
});
|
||||
|
||||
test("should protect the user from its ally's multi-target move", async () => {
|
||||
game.override.enemyMoveset([MoveId.SPLASH]);
|
||||
it("should increment (but not respect) other protection moves' fail counters", async () => {
|
||||
game.override.battleStyle("single");
|
||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD, SpeciesId.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerField();
|
||||
const enemyPokemon = game.scene.getEnemyField();
|
||||
const charizard = game.scene.getPlayerPokemon()!;
|
||||
// force protect to fail on anything other than a guaranteed success
|
||||
vi.spyOn(charizard, "randBattleSeedInt").mockReturnValue(1);
|
||||
|
||||
game.move.select(MoveId.WIDE_GUARD);
|
||||
game.move.select(MoveId.SURF, 1);
|
||||
await game.toNextTurn();
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
expect(charizard.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
|
||||
expect(leadPokemon[0].hp).toBe(leadPokemon[0].getMaxHp());
|
||||
enemyPokemon.forEach(p => expect(p.hp).toBeLessThan(p.getMaxHp()));
|
||||
// ignored fail chance
|
||||
game.move.select(MoveId.WIDE_GUARD);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(charizard.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
|
||||
game.move.select(MoveId.SPIKY_SHIELD);
|
||||
await game.toNextTurn();
|
||||
|
||||
// ignored fail chance
|
||||
expect(charizard.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
});
|
||||
});
|
||||
|
@ -39,15 +39,6 @@ describe("Move - Wish", () => {
|
||||
.enemyLevel(100);
|
||||
});
|
||||
|
||||
/**
|
||||
* Expect that wish is active with the specified number of attacks.
|
||||
* @param numAttacks - The number of wish instances that should be queued; default `1`
|
||||
*/
|
||||
function expectWishActive(numAttacks = 1) {
|
||||
const wishes = game.scene.arena.positionalTagManager["tags"].filter(t => t.tagType === PositionalTagType.WISH);
|
||||
expect(wishes).toHaveLength(numAttacks);
|
||||
}
|
||||
|
||||
it("should heal the Pokemon in the current slot for 50% of the user's maximum HP", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.ALOMOMOLA, SpeciesId.BLISSEY]);
|
||||
|
||||
@ -58,19 +49,19 @@ describe("Move - Wish", () => {
|
||||
game.move.use(MoveId.WISH);
|
||||
await game.toNextTurn();
|
||||
|
||||
expectWishActive();
|
||||
expect(game).toHavePositionalTag(PositionalTagType.WISH);
|
||||
|
||||
game.doSwitchPokemon(1);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expectWishActive(0);
|
||||
expect(game).toHavePositionalTag(PositionalTagType.WISH, 0);
|
||||
expect(game.textInterceptor.logs).toContain(
|
||||
i18next.t("arenaTag:wishTagOnAdd", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(alomomola),
|
||||
}),
|
||||
);
|
||||
expect(alomomola.hp).toBe(1);
|
||||
expect(blissey.hp).toBe(toDmgValue(alomomola.getMaxHp() / 2) + 1);
|
||||
expect(alomomola).toHaveHp(1);
|
||||
expect(blissey).toHaveHp(toDmgValue(alomomola.getMaxHp() / 2) + 1);
|
||||
});
|
||||
|
||||
it("should work if the user has full HP, but not if it already has an active Wish", async () => {
|
||||
@ -82,13 +73,13 @@ describe("Move - Wish", () => {
|
||||
game.move.use(MoveId.WISH);
|
||||
await game.toNextTurn();
|
||||
|
||||
expectWishActive();
|
||||
expect(game).toHavePositionalTag(PositionalTagType.WISH);
|
||||
|
||||
game.move.use(MoveId.WISH);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(alomomola.hp).toBe(toDmgValue(alomomola.getMaxHp() / 2) + 1);
|
||||
expect(alomomola.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
expect(alomomola).toHaveUsedMove({ result: MoveResult.FAIL });
|
||||
});
|
||||
|
||||
it("should function independently of Future Sight", async () => {
|
||||
@ -103,7 +94,8 @@ describe("Move - Wish", () => {
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.toNextTurn();
|
||||
|
||||
expectWishActive(1);
|
||||
expect(game).toHavePositionalTag(PositionalTagType.WISH);
|
||||
expect(game).toHavePositionalTag(PositionalTagType.DELAYED_ATTACK);
|
||||
});
|
||||
|
||||
it("should work in double battles and trigger in order of creation", async () => {
|
||||
@ -127,7 +119,7 @@ describe("Move - Wish", () => {
|
||||
await game.setTurnOrder(oldOrder.map(p => p.getBattlerIndex()));
|
||||
await game.toNextTurn();
|
||||
|
||||
expectWishActive(4);
|
||||
expect(game).toHavePositionalTag(PositionalTagType.WISH, 4);
|
||||
|
||||
// Lower speed to change turn order
|
||||
alomomola.setStatStage(Stat.SPD, 6);
|
||||
@ -141,7 +133,7 @@ describe("Move - Wish", () => {
|
||||
await game.phaseInterceptor.to("PositionalTagPhase");
|
||||
|
||||
// all wishes have activated and added healing phases
|
||||
expectWishActive(0);
|
||||
expect(game).toHavePositionalTag(PositionalTagType.WISH, 0);
|
||||
|
||||
const healPhases = game.scene.phaseManager.phaseQueue.filter(p => p.is("PokemonHealPhase"));
|
||||
expect(healPhases).toHaveLength(4);
|
||||
@ -165,14 +157,14 @@ describe("Move - Wish", () => {
|
||||
game.move.use(MoveId.WISH, BattlerIndex.PLAYER_2);
|
||||
await game.toNextTurn();
|
||||
|
||||
expectWishActive();
|
||||
expect(game).toHavePositionalTag(PositionalTagType.WISH);
|
||||
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER);
|
||||
game.move.use(MoveId.MEMENTO, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
// Wish went away without doing anything
|
||||
expectWishActive(0);
|
||||
expect(game).toHavePositionalTag(PositionalTagType.WISH, 0);
|
||||
expect(game.textInterceptor.logs).not.toContain(
|
||||
i18next.t("arenaTag:wishTagOnAdd", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(blissey),
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { getOnelineDiffStr } from "#test/test-utils/string-utils";
|
||||
import { receivedStr } from "#test/test-utils/test-utils";
|
||||
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||
|
||||
/**
|
||||
@ -14,22 +15,22 @@ export function toEqualArrayUnsorted(
|
||||
): SyncExpectationResult {
|
||||
if (!Array.isArray(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected an array, but got ${this.utils.stringify(received)}!`,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to receive an array, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
||||
if (received.length !== expected.length) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected to receive array of length ${received.length}, but got ${expected.length} instead!`,
|
||||
actual: received,
|
||||
message: () => `Expected to receive an array of length ${received.length}, but got ${expected.length} instead!`,
|
||||
expected,
|
||||
actual: received,
|
||||
};
|
||||
}
|
||||
|
||||
const actualSorted = received.slice().sort();
|
||||
const expectedSorted = expected.slice().sort();
|
||||
const actualSorted = received.toSorted();
|
||||
const expectedSorted = expected.toSorted();
|
||||
const pass = this.equals(actualSorted, expectedSorted, [...this.customTesters, this.utils.iterableEquality]);
|
||||
|
||||
const actualStr = getOnelineDiffStr.call(this, actualSorted);
|
||||
|
@ -21,7 +21,7 @@ export function toHaveAbilityApplied(
|
||||
): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to recieve a Pokemon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
79
test/test-utils/matchers/to-have-arena-tag.ts
Normal file
79
test/test-utils/matchers/to-have-arena-tag.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import type { ArenaTag, ArenaTagTypeMap } from "#data/arena-tag";
|
||||
import type { ArenaTagSide } from "#enums/arena-tag-side";
|
||||
import type { ArenaTagType } from "#enums/arena-tag-type";
|
||||
import type { OneOther } from "#test/@types/test-helpers";
|
||||
// biome-ignore lint/correctness/noUnusedImports: TSDoc
|
||||
import type { GameManager } from "#test/test-utils/game-manager";
|
||||
import { getOnelineDiffStr } from "#test/test-utils/string-utils";
|
||||
import { isGameManagerInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||
import type { NonFunctionPropertiesRecursive } from "#types/type-helpers";
|
||||
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||
|
||||
// intersection required to preserve T for inferences
|
||||
export type toHaveArenaTagOptions<T extends ArenaTagType> = OneOther<ArenaTagTypeMap[T], "tagType" | "side"> & {
|
||||
tagType: T;
|
||||
};
|
||||
|
||||
/**
|
||||
* Matcher to check if the {@linkcode Arena} has a given {@linkcode ArenaTag} active.
|
||||
* @param received - The object to check. Should be the current {@linkcode GameManager}.
|
||||
* @param expectedTag - The {@linkcode ArenaTagType} of the desired tag, or a partially-filled object
|
||||
* containing the desired properties
|
||||
* @param side - The {@linkcode ArenaTagSide | side of the field} the tag should affect, or
|
||||
* {@linkcode ArenaTagSide.BOTH} to check both sides
|
||||
* @returns The result of the matching
|
||||
*/
|
||||
export function toHaveArenaTag<T extends ArenaTagType>(
|
||||
this: MatcherState,
|
||||
received: unknown,
|
||||
// simplified types used for brevity; full overloads are in `vitest.d.ts`
|
||||
expectedTag: T | (Partial<NonFunctionPropertiesRecursive<ArenaTag>> & { tagType: T; side: ArenaTagSide }),
|
||||
side?: ArenaTagSide,
|
||||
): SyncExpectationResult {
|
||||
if (!isGameManagerInstance(received)) {
|
||||
return {
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to recieve a GameManager, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!received.scene?.arena) {
|
||||
return {
|
||||
pass: this.isNot,
|
||||
message: () => `Expected GameManager.${received.scene ? "scene.arena" : "scene"} to be defined!`,
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof expectedTag === "string") {
|
||||
// Coerce lone `tagType`s into objects
|
||||
// Bangs are ok as we enforce safety via overloads
|
||||
expectedTag = { tagType: expectedTag, side: side! };
|
||||
}
|
||||
|
||||
// We need to get all tags for the case of checking properties of a tag present on both sides of the arena
|
||||
const tags = received.scene.arena.findTagsOnSide(t => t.tagType === expectedTag.tagType, expectedTag.side);
|
||||
if (tags.length === 0) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected the Arena to have a tag of type ${expectedTag.tagType}, but it didn't!`,
|
||||
expected: expectedTag.tagType,
|
||||
actual: received.scene.arena.tags.map(t => t.tagType),
|
||||
};
|
||||
}
|
||||
|
||||
// Pass if any of the matching tags meet our criteria
|
||||
const pass = tags.some(tag =>
|
||||
this.equals(tag, expectedTag, [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality]),
|
||||
);
|
||||
|
||||
const expectedStr = getOnelineDiffStr.call(this, expectedTag);
|
||||
return {
|
||||
pass,
|
||||
message: () =>
|
||||
pass
|
||||
? `Expected the Arena to NOT have a tag matching ${expectedStr}, but it did!`
|
||||
: `Expected the Arena to have a tag matching ${expectedStr}, but it didn't!`,
|
||||
expected: expectedTag,
|
||||
actual: tags,
|
||||
};
|
||||
}
|
@ -6,7 +6,7 @@ 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 {
|
||||
export interface toHaveEffectiveStatOptions {
|
||||
/**
|
||||
* The target {@linkcode Pokemon}
|
||||
* @see {@linkcode Pokemon.getEffectiveStat}
|
||||
@ -30,7 +30,7 @@ export interface ToHaveEffectiveStatMatcherOptions {
|
||||
* @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}
|
||||
* @param options - The {@linkcode toHaveEffectiveStatOptions}
|
||||
* @returns Whether the matcher passed
|
||||
*/
|
||||
export function toHaveEffectiveStat(
|
||||
@ -38,11 +38,11 @@ export function toHaveEffectiveStat(
|
||||
received: unknown,
|
||||
stat: EffectiveStat,
|
||||
expectedValue: number,
|
||||
{ enemy, move, isCritical = false }: ToHaveEffectiveStatMatcherOptions = {},
|
||||
{ enemy, move, isCritical = false }: toHaveEffectiveStatOptions = {},
|
||||
): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||
export function toHaveFainted(this: MatcherState, received: unknown): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||
export function toHaveFullHp(this: MatcherState, received: unknown): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||
export function toHaveHp(this: MatcherState, received: unknown, expectedHp: number): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
108
test/test-utils/matchers/to-have-positional-tag.ts
Normal file
108
test/test-utils/matchers/to-have-positional-tag.ts
Normal file
@ -0,0 +1,108 @@
|
||||
// biome-ignore-start lint/correctness/noUnusedImports: TSDoc
|
||||
import type { GameManager } from "#test/test-utils/game-manager";
|
||||
// biome-ignore-end lint/correctness/noUnusedImports: TSDoc
|
||||
|
||||
import type { SerializedPositionalTag, serializedPosTagMap } from "#data/positional-tags/load-positional-tag";
|
||||
import type { PositionalTagType } from "#enums/positional-tag-type";
|
||||
import type { OneOther } from "#test/@types/test-helpers";
|
||||
import { getOnelineDiffStr } from "#test/test-utils/string-utils";
|
||||
import { isGameManagerInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||
import { toTitleCase } from "#utils/strings";
|
||||
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||
|
||||
export type toHavePositionalTagOptions<P extends PositionalTagType> = OneOther<serializedPosTagMap[P], "tagType"> & {
|
||||
tagType: P;
|
||||
};
|
||||
|
||||
/**
|
||||
* Matcher to check if the {@linkcode Arena} has a certain number of {@linkcode PositionalTag}s active.
|
||||
* @param received - The object to check. Should be the current {@linkcode GameManager}
|
||||
* @param expectedTag - The {@linkcode PositionalTagType} of the desired tag, or a partially-filled {@linkcode PositionalTag}
|
||||
* containing the desired properties
|
||||
* @param count - The number of tags that should be active; defaults to `1` and must be within the range `[0, 4]`
|
||||
* @returns The result of the matching
|
||||
*/
|
||||
export function toHavePositionalTag<P extends PositionalTagType>(
|
||||
this: MatcherState,
|
||||
received: unknown,
|
||||
// simplified types used for brevity; full overloads are in `vitest.d.ts`
|
||||
expectedTag: P | (Partial<SerializedPositionalTag> & { tagType: P }),
|
||||
count = 1,
|
||||
): SyncExpectationResult {
|
||||
if (!isGameManagerInstance(received)) {
|
||||
return {
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to recieve a GameManager, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!received.scene?.arena?.positionalTagManager) {
|
||||
return {
|
||||
pass: this.isNot,
|
||||
message: () =>
|
||||
`Expected GameManager.${received.scene?.arena ? "scene.arena.positionalTagManager" : received.scene ? "scene.arena" : "scene"} to be defined!`,
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Increase limit if triple battles are added
|
||||
if (count < 0 || count > 4) {
|
||||
return {
|
||||
pass: this.isNot,
|
||||
message: () => `Expected count to be between 0 and 4, but got ${count} instead!`,
|
||||
};
|
||||
}
|
||||
|
||||
const allTags = received.scene.arena.positionalTagManager.tags;
|
||||
const tagType = typeof expectedTag === "string" ? expectedTag : expectedTag.tagType;
|
||||
const matchingTags = allTags.filter(t => t.tagType === tagType);
|
||||
|
||||
// If checking exclusively tag type, check solely the number of matching tags on field
|
||||
if (typeof expectedTag === "string") {
|
||||
const pass = matchingTags.length === count;
|
||||
const expectedStr = getPosTagStr(expectedTag);
|
||||
|
||||
return {
|
||||
pass,
|
||||
message: () =>
|
||||
pass
|
||||
? `Expected the Arena to NOT have ${count} ${expectedStr} active, but it did!`
|
||||
: `Expected the Arena to have ${count} ${expectedStr} active, but got ${matchingTags.length} instead!`,
|
||||
expected: expectedTag,
|
||||
actual: allTags,
|
||||
};
|
||||
}
|
||||
|
||||
// Check for equality with the provided object
|
||||
if (matchingTags.length === 0) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected the Arena to have a tag of type ${expectedTag.tagType}, but it didn't!`,
|
||||
expected: expectedTag.tagType,
|
||||
actual: received.scene.arena.tags.map(t => t.tagType),
|
||||
};
|
||||
}
|
||||
|
||||
// Pass if any of the matching tags meet our criteria
|
||||
const pass = matchingTags.some(tag =>
|
||||
this.equals(tag, expectedTag, [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality]),
|
||||
);
|
||||
|
||||
const expectedStr = getOnelineDiffStr.call(this, expectedTag);
|
||||
return {
|
||||
pass,
|
||||
message: () =>
|
||||
pass
|
||||
? `Expected the Arena to NOT have a tag matching ${expectedStr}, but it did!`
|
||||
: `Expected the Arena to have a tag matching ${expectedStr}, but it didn't!`,
|
||||
expected: expectedTag,
|
||||
actual: matchingTags,
|
||||
};
|
||||
}
|
||||
|
||||
function getPosTagStr(pType: PositionalTagType, count = 1): string {
|
||||
let ret = toTitleCase(pType) + "Tag";
|
||||
if (count > 1) {
|
||||
ret += "s";
|
||||
}
|
||||
return ret;
|
||||
}
|
@ -23,14 +23,14 @@ export function toHaveStatStage(
|
||||
): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
||||
if (expectedStage < -6 || expectedStage > 6) {
|
||||
return {
|
||||
pass: false,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected ${expectedStage} to be within the range [-6, 6]!`,
|
||||
};
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ export function toHaveStatusEffect(
|
||||
): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
@ -37,10 +37,8 @@ export function toHaveStatusEffect(
|
||||
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 === "object" && actualEffect !== expectedStatus.effect) {
|
||||
expectedStatus = expectedStatus.effect;
|
||||
}
|
||||
|
||||
if (typeof expectedStatus === "number") {
|
||||
|
@ -24,7 +24,7 @@ export function toHaveTakenDamage(
|
||||
): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
@ -20,15 +20,15 @@ export function toHaveTerrain(
|
||||
): SyncExpectationResult {
|
||||
if (!isGameManagerInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected GameManager, but got ${receivedStr(received)}!`,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to receive a GameManager, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!received.scene?.arena) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected GameManager.${received.scene ? "scene" : "scene.arena"} to be defined!`,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected GameManager.${received.scene ? "scene.arena" : "scene"} to be defined!`,
|
||||
};
|
||||
}
|
||||
|
||||
@ -41,8 +41,8 @@ export function toHaveTerrain(
|
||||
pass,
|
||||
message: () =>
|
||||
pass
|
||||
? `Expected Arena to NOT have ${expectedStr} active, but it did!`
|
||||
: `Expected Arena to have ${expectedStr} active, but got ${actualStr} instead!`,
|
||||
? `Expected the Arena to NOT have ${expectedStr} active, but it did!`
|
||||
: `Expected the Arena to have ${expectedStr} active, but got ${actualStr} instead!`,
|
||||
expected: expectedTerrainType,
|
||||
actual,
|
||||
};
|
||||
|
@ -7,10 +7,16 @@ import { isPokemonInstance, receivedStr } from "../test-utils";
|
||||
|
||||
export interface toHaveTypesOptions {
|
||||
/**
|
||||
* Whether to enforce exact matches (`true`) or superset matches (`false`).
|
||||
* @defaultValue `true`
|
||||
* Value dictating the strength of the enforced typing match.
|
||||
*
|
||||
* Possible values (in ascending order of strength) are:
|
||||
* - `"ordered"`: Enforce that the {@linkcode Pokemon}'s types are identical **and in the same order**
|
||||
* - `"unordered"`: Enforce that the {@linkcode Pokemon}'s types are identical **without checking order**
|
||||
* - `"superset"`: Enforce that the {@linkcode Pokemon}'s types are **a superset of** the expected types
|
||||
* (all must be present, but extras can be there)
|
||||
* @defaultValue `"unordered"`
|
||||
*/
|
||||
exact?: boolean;
|
||||
mode?: "ordered" | "unordered" | "superset";
|
||||
/**
|
||||
* Optional arguments to pass to {@linkcode Pokemon.getTypes}.
|
||||
*/
|
||||
@ -18,35 +24,46 @@ export interface toHaveTypesOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 options - The {@linkcode toHaveTypesOptions | options} for this matcher
|
||||
* Matcher that checks if a {@linkcode Pokemon}'s typing is as expected.
|
||||
* @param received - The object to check. Should be a {@linkcode Pokemon}
|
||||
* @param expectedTypes - An array of one or more {@linkcode PokemonType}s to compare against.
|
||||
* @param mode - The mode to perform the matching;
|
||||
* @returns The result of the matching
|
||||
*/
|
||||
export function toHaveTypes(
|
||||
this: MatcherState,
|
||||
received: unknown,
|
||||
expected: [PokemonType, ...PokemonType[]],
|
||||
options: toHaveTypesOptions = {},
|
||||
expectedTypes: [PokemonType, ...PokemonType[]],
|
||||
{ mode = "unordered", args = [] }: toHaveTypesOptions = {},
|
||||
): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to recieve a Pokémon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
||||
const actualTypes = received.getTypes(...(options.args ?? [])).sort();
|
||||
const expectedTypes = expected.slice().sort();
|
||||
// Return early if no types were passed in
|
||||
if (expectedTypes.length === 0) {
|
||||
return {
|
||||
pass: this.isNot,
|
||||
message: () => "Expected to receive a non-empty array of PokemonTypes!",
|
||||
};
|
||||
}
|
||||
|
||||
// Avoid sorting the types if strict ordering is desired
|
||||
const actualSorted = mode === "ordered" ? received.getTypes(...args) : received.getTypes(...args).toSorted();
|
||||
const expectedSorted = mode === "ordered" ? expectedTypes : expectedTypes.toSorted();
|
||||
|
||||
// Exact matches do not care about subset equality
|
||||
const matchers = options.exact
|
||||
? [...this.customTesters, this.utils.iterableEquality]
|
||||
: [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality];
|
||||
const pass = this.equals(actualTypes, expectedTypes, matchers);
|
||||
const matchers =
|
||||
mode === "superset"
|
||||
? [...this.customTesters, this.utils.iterableEquality]
|
||||
: [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality];
|
||||
const pass = this.equals(actualSorted, expectedSorted, matchers);
|
||||
|
||||
const actualStr = stringifyEnumArray(PokemonType, actualTypes);
|
||||
const expectedStr = stringifyEnumArray(PokemonType, expectedTypes);
|
||||
const actualStr = stringifyEnumArray(PokemonType, actualSorted);
|
||||
const expectedStr = stringifyEnumArray(PokemonType, expectedSorted);
|
||||
const pkmName = getPokemonNameWithAffix(received);
|
||||
|
||||
return {
|
||||
@ -55,7 +72,7 @@ export function toHaveTypes(
|
||||
pass
|
||||
? `Expected ${pkmName} to NOT have types ${expectedStr}, but it did!`
|
||||
: `Expected ${pkmName} to have types ${expectedStr}, but got ${actualStr} instead!`,
|
||||
expected: expectedTypes,
|
||||
actual: actualTypes,
|
||||
expected: expectedSorted,
|
||||
actual: actualSorted,
|
||||
};
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ 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,
|
||||
* @param expectedMove - 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)
|
||||
@ -22,12 +22,12 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||
export function toHaveUsedMove(
|
||||
this: MatcherState,
|
||||
received: unknown,
|
||||
expectedResult: MoveId | AtLeastOne<TurnMove>,
|
||||
expectedMove: MoveId | AtLeastOne<TurnMove>,
|
||||
index = 0,
|
||||
): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
@ -37,34 +37,33 @@ export function toHaveUsedMove(
|
||||
|
||||
if (move === undefined) {
|
||||
return {
|
||||
pass: false,
|
||||
pass: this.isNot,
|
||||
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 };
|
||||
if (typeof expectedMove === "number") {
|
||||
expectedMove = { move: expectedMove };
|
||||
}
|
||||
|
||||
const moveIndexStr = index === 0 ? "last move" : `${getOrdinal(index)} most recent move`;
|
||||
|
||||
const pass = this.equals(move, expectedResult, [
|
||||
const pass = this.equals(move, expectedMove, [
|
||||
...this.customTesters,
|
||||
this.utils.subsetEquality,
|
||||
this.utils.iterableEquality,
|
||||
]);
|
||||
|
||||
const expectedStr = getOnelineDiffStr.call(this, expectedResult);
|
||||
const expectedStr = getOnelineDiffStr.call(this, expectedMove);
|
||||
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,
|
||||
: `Expected ${pkmName}'s ${moveIndexStr} to match ${expectedStr}, but it didn't!`,
|
||||
expected: expectedMove,
|
||||
actual: move,
|
||||
};
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ 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 moveId - 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
|
||||
@ -23,12 +23,12 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||
export function toHaveUsedPP(
|
||||
this: MatcherState,
|
||||
received: unknown,
|
||||
expectedMove: MoveId,
|
||||
moveId: MoveId,
|
||||
ppUsed: number | "all",
|
||||
): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
@ -36,22 +36,22 @@ export function toHaveUsedPP(
|
||||
const override = received.isPlayer() ? Overrides.MOVESET_OVERRIDE : Overrides.OPP_MOVESET_OVERRIDE;
|
||||
if (coerceArray(override).length > 0) {
|
||||
return {
|
||||
pass: false,
|
||||
pass: this.isNot,
|
||||
message: () =>
|
||||
`Cannot test for PP consumption with ${received.isPlayer() ? "player" : "enemy"} moveset overrides active!`,
|
||||
};
|
||||
}
|
||||
|
||||
const pkmName = getPokemonNameWithAffix(received);
|
||||
const moveStr = getEnumStr(MoveId, expectedMove);
|
||||
const moveStr = getEnumStr(MoveId, moveId);
|
||||
|
||||
const movesetMoves = received.getMoveset().filter(pm => pm.moveId === expectedMove);
|
||||
const movesetMoves = received.getMoveset().filter(pm => pm.moveId === moveId);
|
||||
if (movesetMoves.length !== 1) {
|
||||
return {
|
||||
pass: false,
|
||||
pass: this.isNot,
|
||||
message: () =>
|
||||
`Expected MoveId.${moveStr} to appear in ${pkmName}'s moveset exactly once, but got ${movesetMoves.length} times!`,
|
||||
expected: expectedMove,
|
||||
expected: moveId,
|
||||
actual: received.getMoveset(),
|
||||
};
|
||||
}
|
||||
|
@ -20,15 +20,15 @@ export function toHaveWeather(
|
||||
): SyncExpectationResult {
|
||||
if (!isGameManagerInstance(received)) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected GameManager, but got ${receivedStr(received)}!`,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected to receive a GameManager, but got ${receivedStr(received)}!`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!received.scene?.arena) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Expected GameManager.${received.scene ? "scene" : "scene.arena"} to be defined!`,
|
||||
pass: this.isNot,
|
||||
message: () => `Expected GameManager.${received.scene ? "scene.arena" : "scene"} to be defined!`,
|
||||
};
|
||||
}
|
||||
|
||||
@ -41,8 +41,8 @@ export function toHaveWeather(
|
||||
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 the Arena to NOT have ${expectedStr} weather active, but it did!`
|
||||
: `Expected the Arena to have ${expectedStr} weather active, but got ${actualStr} instead!`,
|
||||
expected: expectedWeatherType,
|
||||
actual,
|
||||
};
|
||||
|
@ -34,10 +34,10 @@ interface getEnumStrOptions {
|
||||
* @returns The stringified representation of `val` as dictated by the options.
|
||||
* @example
|
||||
* ```ts
|
||||
* enum fakeEnum {
|
||||
* ONE: 1,
|
||||
* TWO: 2,
|
||||
* THREE: 3,
|
||||
* enum testEnum {
|
||||
* 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)"
|
||||
@ -174,10 +174,14 @@ export function getStatName(s: Stat): string {
|
||||
* 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
|
||||
* @example
|
||||
* ```ts
|
||||
* const diff = getOnelineDiffStr.call(this, obj)
|
||||
* ```
|
||||
*/
|
||||
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}");
|
||||
.replace(/,(\s*)}$/g, "$1}"); // Trim trailing commas
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user