mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-06 23:49:26 +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 {
|
getCondition(): MoveConditionFunc {
|
||||||
return ((user, target, move): boolean => {
|
return ((user, target, move): boolean => {
|
||||||
let timesUsed = 0;
|
let timesUsed = 0;
|
||||||
const moveHistory = user.getLastXMoves();
|
|
||||||
let turnMove: TurnMove | undefined;
|
|
||||||
|
|
||||||
while (moveHistory.length) {
|
for (const turnMove of user.getLastXMoves(-1).slice()) {
|
||||||
turnMove = moveHistory.shift();
|
if (
|
||||||
if (!allMoves[turnMove?.move ?? MoveId.NONE].hasAttr("ProtectAttr") || turnMove?.result !== MoveResult.SUCCESS) {
|
// 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;
|
break;
|
||||||
}
|
}
|
||||||
timesUsed++;
|
|
||||||
|
timesUsed++
|
||||||
}
|
}
|
||||||
if (timesUsed) {
|
|
||||||
return !user.randBattleSeedInt(Math.pow(3, timesUsed));
|
return timesUsed === 0 || user.randBattleSeedInt(Math.pow(3, timesUsed)) === 0;
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -253,7 +253,6 @@ export class PokemonTempSummonData {
|
|||||||
* Only currently used for positioning the battle cursor.
|
* Only currently used for positioning the battle cursor.
|
||||||
*/
|
*/
|
||||||
turnCount = 1;
|
turnCount = 1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The number of turns this pokemon has spent in the active position since the start of the wave
|
* The number of turns this pokemon has spent in the active position since the start of the wave
|
||||||
* without switching out.
|
* without switching out.
|
||||||
|
@ -5094,6 +5094,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
*/
|
*/
|
||||||
resetWaveData(): void {
|
resetWaveData(): void {
|
||||||
this.waveData = new PokemonWaveData();
|
this.waveData = new PokemonWaveData();
|
||||||
|
this.tempSummonData.waveTurnCount = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
resetTera(): void {
|
resetTera(): void {
|
||||||
|
@ -58,12 +58,6 @@ export class BattleEndPhase extends BattlePhase {
|
|||||||
globalScene.phaseManager.unshiftNew("GameOverPhase", true);
|
globalScene.phaseManager.unshiftNew("GameOverPhase", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const pokemon of globalScene.getField()) {
|
|
||||||
if (pokemon) {
|
|
||||||
pokemon.tempSummonData.waveTurnCount = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const pokemon of globalScene.getPokemonAllowedInBattle()) {
|
for (const pokemon of globalScene.getPokemonAllowedInBattle()) {
|
||||||
applyAbAttrs("PostBattleAbAttr", { pokemon, victory: this.isVictory });
|
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 { TerrainType } from "#app/data/terrain";
|
||||||
|
import type { ArenaTag } from "#data/arena-tag";
|
||||||
import type { AbilityId } from "#enums/ability-id";
|
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 { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
import type { MoveId } from "#enums/move-id";
|
import type { MoveId } from "#enums/move-id";
|
||||||
import type { PokemonType } from "#enums/pokemon-type";
|
import type { PokemonType } from "#enums/pokemon-type";
|
||||||
import type { BattleStat, EffectiveStat, Stat } from "#enums/stat";
|
import type { BattleStat, EffectiveStat, Stat } from "#enums/stat";
|
||||||
import type { StatusEffect } from "#enums/status-effect";
|
import type { StatusEffect } from "#enums/status-effect";
|
||||||
import type { WeatherType } from "#enums/weather-type";
|
import type { WeatherType } from "#enums/weather-type";
|
||||||
|
import type { Arena } from "#field/arena";
|
||||||
import type { Pokemon } from "#field/pokemon";
|
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 { 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 { TurnMove } from "#types/turn-move";
|
||||||
import type { AtLeastOne } from "#types/type-helpers";
|
import type { AtLeastOne } from "#types/type-helpers";
|
||||||
|
import type { toDmgValue } from "utils/common";
|
||||||
import type { expect } from "vitest";
|
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 Overrides from "#app/overrides";
|
||||||
|
import type { ArenaTagSide } from "#enums/arena-tag-side";
|
||||||
import type { PokemonMove } from "#moves/pokemon-move";
|
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" {
|
declare module "vitest" {
|
||||||
interface Assertion {
|
interface Assertion<T> {
|
||||||
/**
|
/**
|
||||||
* Check whether an array contains EXACTLY the given items (in any order).
|
* 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
|
* @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(expected: T[]): void;
|
||||||
|
|
||||||
/**
|
// #region Arena Matchers
|
||||||
* 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;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether the current {@linkcode WeatherType} is as expected.
|
* Check whether the current {@linkcode WeatherType} is as expected.
|
||||||
@ -80,9 +53,63 @@ declare module "vitest" {
|
|||||||
toHaveTerrain(expectedTerrainType: TerrainType): void;
|
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}.
|
* 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}.
|
* 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;
|
toHaveAbilityApplied(expectedAbilityId: AbilityId): void;
|
||||||
|
|
||||||
@ -116,24 +143,36 @@ declare module "vitest" {
|
|||||||
*/
|
*/
|
||||||
toHaveHp(expectedHp: number): void;
|
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}).
|
* Check whether a {@linkcode Pokemon} is currently fainted (as determined by {@linkcode Pokemon.isFainted}).
|
||||||
* @remarks
|
* @remarks
|
||||||
* When checking whether an enemy wild Pokemon is fainted, one must reference it in a variable _before_ the fainting effect occurs
|
* 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 GC'ed and rendered `undefined`.
|
* as otherwise the Pokemon will be removed from the field and garbage collected.
|
||||||
*/
|
*/
|
||||||
toHaveFainted(): void;
|
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.
|
* 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,
|
* @param ppUsed - The numerical amount of PP that should have been consumed,
|
||||||
* or `all` to indicate the move should be _out_ of PP
|
* or `all` to indicate the move should be _out_ of PP
|
||||||
* @remarks
|
* @remarks
|
||||||
* If the Pokemon's moveset has been set via {@linkcode Overrides.MOVESET_OVERRIDE}/{@linkcode Overrides.OPP_MOVESET_OVERRIDE},
|
* If the Pokemon's moveset has been set via {@linkcode Overrides.MOVESET_OVERRIDE}/{@linkcode Overrides.OPP_MOVESET_OVERRIDE}
|
||||||
* does not contain {@linkcode expectedMove}
|
* or does not contain exactly 1 copy of {@linkcode moveId}, this will fail the test.
|
||||||
* or contains the desired move more than once, 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 { toEqualArrayUnsorted } from "#test/test-utils/matchers/to-equal-array-unsorted";
|
||||||
import { toHaveAbilityApplied } from "#test/test-utils/matchers/to-have-ability-applied";
|
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 { toHaveBattlerTag } from "#test/test-utils/matchers/to-have-battler-tag";
|
||||||
import { toHaveEffectiveStat } from "#test/test-utils/matchers/to-have-effective-stat";
|
import { toHaveEffectiveStat } from "#test/test-utils/matchers/to-have-effective-stat";
|
||||||
import { toHaveFainted } from "#test/test-utils/matchers/to-have-fainted";
|
import { toHaveFainted } from "#test/test-utils/matchers/to-have-fainted";
|
||||||
import { toHaveFullHp } from "#test/test-utils/matchers/to-have-full-hp";
|
import { toHaveFullHp } from "#test/test-utils/matchers/to-have-full-hp";
|
||||||
import { toHaveHp } from "#test/test-utils/matchers/to-have-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 { toHaveStatStage } from "#test/test-utils/matchers/to-have-stat-stage";
|
||||||
import { toHaveStatusEffect } from "#test/test-utils/matchers/to-have-status-effect";
|
import { toHaveStatusEffect } from "#test/test-utils/matchers/to-have-status-effect";
|
||||||
import { toHaveTakenDamage } from "#test/test-utils/matchers/to-have-taken-damage";
|
import { toHaveTakenDamage } from "#test/test-utils/matchers/to-have-taken-damage";
|
||||||
@ -22,18 +24,20 @@ import { expect } from "vitest";
|
|||||||
|
|
||||||
expect.extend({
|
expect.extend({
|
||||||
toEqualArrayUnsorted,
|
toEqualArrayUnsorted,
|
||||||
|
toHaveWeather,
|
||||||
|
toHaveTerrain,
|
||||||
|
toHaveArenaTag,
|
||||||
|
toHavePositionalTag,
|
||||||
toHaveTypes,
|
toHaveTypes,
|
||||||
toHaveUsedMove,
|
toHaveUsedMove,
|
||||||
toHaveEffectiveStat,
|
toHaveEffectiveStat,
|
||||||
toHaveTakenDamage,
|
|
||||||
toHaveWeather,
|
|
||||||
toHaveTerrain,
|
|
||||||
toHaveFullHp,
|
|
||||||
toHaveStatusEffect,
|
toHaveStatusEffect,
|
||||||
toHaveStatStage,
|
toHaveStatStage,
|
||||||
toHaveBattlerTag,
|
toHaveBattlerTag,
|
||||||
toHaveAbilityApplied,
|
toHaveAbilityApplied,
|
||||||
toHaveHp,
|
toHaveHp,
|
||||||
|
toHaveTakenDamage,
|
||||||
|
toHaveFullHp,
|
||||||
toHaveFainted,
|
toHaveFainted,
|
||||||
toHaveUsedPP,
|
toHaveUsedPP,
|
||||||
});
|
});
|
||||||
|
@ -5,7 +5,7 @@ import { SpeciesId } from "#enums/species-id";
|
|||||||
import { StatusEffect } from "#enums/status-effect";
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
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, test } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
describe("Moves - Baneful Bunker", () => {
|
describe("Moves - Baneful Bunker", () => {
|
||||||
let phaserGame: Phaser.Game;
|
let phaserGame: Phaser.Game;
|
||||||
@ -26,55 +26,51 @@ describe("Moves - Baneful Bunker", () => {
|
|||||||
|
|
||||||
game.override
|
game.override
|
||||||
.battleStyle("single")
|
.battleStyle("single")
|
||||||
.moveset(MoveId.SLASH)
|
.moveset([MoveId.SLASH, MoveId.FLASH_CANNON])
|
||||||
.enemySpecies(SpeciesId.SNORLAX)
|
.enemySpecies(SpeciesId.TOXAPEX)
|
||||||
.enemyAbility(AbilityId.INSOMNIA)
|
.enemyAbility(AbilityId.INSOMNIA)
|
||||||
.enemyMoveset(MoveId.BANEFUL_BUNKER)
|
.enemyMoveset(MoveId.BANEFUL_BUNKER)
|
||||||
.startingLevel(100)
|
.startingLevel(100)
|
||||||
.enemyLevel(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();
|
function expectProtected() {
|
||||||
const enemyPokemon = game.field.getEnemyPokemon();
|
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);
|
game.move.select(MoveId.SLASH);
|
||||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||||
await game.phaseInterceptor.to("BerryPhase", false);
|
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]);
|
await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
|
||||||
|
|
||||||
const leadPokemon = game.field.getPlayerPokemon();
|
|
||||||
const enemyPokemon = game.field.getEnemyPokemon();
|
|
||||||
|
|
||||||
game.move.select(MoveId.SLASH);
|
game.move.select(MoveId.SLASH);
|
||||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
await game.phaseInterceptor.to("MoveEndPhase"); // baneful bunker
|
||||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
|
||||||
|
|
||||||
await game.move.forceMiss();
|
await game.move.forceMiss();
|
||||||
|
|
||||||
await game.phaseInterceptor.to("BerryPhase", false);
|
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 () => {
|
it("should block non-contact moves without poisoning attackers", async () => {
|
||||||
game.override.moveset(MoveId.FLASH_CANNON);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
|
await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
|
||||||
|
|
||||||
const leadPokemon = game.field.getPlayerPokemon();
|
const charizard = game.field.getPlayerPokemon();
|
||||||
const enemyPokemon = game.field.getEnemyPokemon();
|
const toxapex = game.field.getEnemyPokemon();
|
||||||
|
|
||||||
game.move.select(MoveId.FLASH_CANNON);
|
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);
|
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 { 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 { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
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 { Stat } from "#enums/stat";
|
import { Stat } from "#enums/stat";
|
||||||
import { BerryPhase } from "#phases/berry-phase";
|
|
||||||
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, test } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
describe("Moves - Crafty Shield", () => {
|
describe("Moves - Crafty Shield", () => {
|
||||||
let phaserGame: Phaser.Game;
|
let phaserGame: Phaser.Game;
|
||||||
@ -27,68 +29,100 @@ describe("Moves - Crafty Shield", () => {
|
|||||||
|
|
||||||
game.override
|
game.override
|
||||||
.battleStyle("double")
|
.battleStyle("double")
|
||||||
.moveset([MoveId.CRAFTY_SHIELD, MoveId.SPLASH, MoveId.SWORDS_DANCE])
|
.enemySpecies(SpeciesId.DUSKNOIR)
|
||||||
.enemySpecies(SpeciesId.SNORLAX)
|
.enemyMoveset(MoveId.GROWL)
|
||||||
.enemyMoveset([MoveId.GROWL])
|
|
||||||
.enemyAbility(AbilityId.INSOMNIA)
|
.enemyAbility(AbilityId.INSOMNIA)
|
||||||
.startingLevel(100)
|
.startingLevel(100)
|
||||||
.enemyLevel(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]);
|
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);
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
game.move.select(MoveId.SPLASH, 1);
|
|
||||||
|
|
||||||
await game.phaseInterceptor.to(BerryPhase, false);
|
expect(charizard.getStatStage(Stat.ATK)).toBe(0);
|
||||||
|
expect(blastoise.getStatStage(Stat.ATK)).toBe(0);
|
||||||
leadPokemon.forEach(p => expect(p.getStatStage(Stat.ATK)).toBe(0));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should not protect the user and allies from attack moves", async () => {
|
it("should not protect the user and allies from attack moves", async () => {
|
||||||
game.override.enemyMoveset([MoveId.TACKLE]);
|
game.override.enemyMoveset(MoveId.TACKLE);
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD, SpeciesId.BLASTOISE]);
|
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.use(MoveId.CRAFTY_SHIELD, BattlerIndex.PLAYER);
|
||||||
game.move.select(MoveId.SPLASH, 1);
|
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(charizard.isFullHp()).toBe(false);
|
||||||
|
expect(blastoise.isFullHp()).toBe(false);
|
||||||
expect(leadPokemon.some(p => p.hp < p.getMaxHp())).toBeTruthy();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should protect the user and allies from moves that ignore other protection", async () => {
|
it("should not block entry hazards and field-targeted moves", async () => {
|
||||||
game.override.enemySpecies(SpeciesId.DUSCLOPS).enemyMoveset([MoveId.CURSE]);
|
game.override.enemyMoveset([MoveId.PERISH_SONG, MoveId.TOXIC_SPIKES]);
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD, SpeciesId.BLASTOISE]);
|
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.use(MoveId.CRAFTY_SHIELD, BattlerIndex.PLAYER);
|
||||||
game.move.select(MoveId.SPLASH, 1);
|
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);
|
expect(game.scene.arena.getTagOnSide(ArenaTagType.TOXIC_SPIKES, ArenaTagSide.PLAYER)).toBeDefined();
|
||||||
|
expect(charizard.getTag(BattlerTagType.PERISH_SONG)).toBeDefined();
|
||||||
leadPokemon.forEach(p => expect(p.getTag(BattlerTagType.CURSED)).toBeUndefined());
|
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]);
|
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.use(MoveId.CRAFTY_SHIELD, BattlerIndex.PLAYER);
|
||||||
game.move.select(MoveId.SWORDS_DANCE, 1);
|
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(charizard.getTag(BattlerTagType.CURSED)).toBeUndefined();
|
||||||
expect(leadPokemon[1].getStatStage(Stat.ATK)).toBe(2);
|
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 { AbilityId } from "#enums/ability-id";
|
||||||
|
import { HitResult } from "#enums/hit-result";
|
||||||
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 { 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, vi } from "vitest";
|
||||||
|
|
||||||
describe("Moves - Endure", () => {
|
describe("Moves - Endure", () => {
|
||||||
let phaserGame: Phaser.Game;
|
let phaserGame: Phaser.Game;
|
||||||
@ -22,7 +23,7 @@ describe("Moves - Endure", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
game = new GameManager(phaserGame);
|
game = new GameManager(phaserGame);
|
||||||
game.override
|
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)
|
.ability(AbilityId.SKILL_LINK)
|
||||||
.startingLevel(100)
|
.startingLevel(100)
|
||||||
.battleStyle("single")
|
.battleStyle("single")
|
||||||
@ -32,7 +33,7 @@ describe("Moves - Endure", () => {
|
|||||||
.enemyMoveset(MoveId.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]);
|
await game.classicMode.startBattle([SpeciesId.ARCEUS]);
|
||||||
|
|
||||||
game.move.select(MoveId.THUNDER);
|
game.move.select(MoveId.THUNDER);
|
||||||
@ -41,7 +42,7 @@ describe("Moves - Endure", () => {
|
|||||||
expect(game.field.getEnemyPokemon().hp).toBe(1);
|
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]);
|
await game.classicMode.startBattle([SpeciesId.ARCEUS]);
|
||||||
|
|
||||||
game.move.select(MoveId.BULLET_SEED);
|
game.move.select(MoveId.BULLET_SEED);
|
||||||
@ -57,30 +58,27 @@ describe("Moves - Endure", () => {
|
|||||||
game.move.select(MoveId.SHEER_COLD);
|
game.move.select(MoveId.SHEER_COLD);
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
expect(enemy.isFainted()).toBeFalsy();
|
expect(enemy.hp).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
// comprehensive indirect damage test copied from Reviver Seed test
|
// comprehensive indirect damage test copied from Reviver Seed test
|
||||||
it.each([
|
it.each([
|
||||||
{ moveType: "Damaging Move Chip Damage", move: MoveId.SALT_CURE },
|
{ moveType: "Damaging Move Chip", move: MoveId.SALT_CURE },
|
||||||
{ moveType: "Chip Damage", move: MoveId.LEECH_SEED },
|
{ moveType: "Status Move Chip", move: MoveId.LEECH_SEED },
|
||||||
{ moveType: "Trapping Chip Damage", move: MoveId.WHIRLPOOL },
|
{ moveType: "Partial Trapping move", move: MoveId.WHIRLPOOL },
|
||||||
{ moveType: "Status Effect Damage", move: MoveId.TOXIC },
|
{ moveType: "Status Effect", move: MoveId.TOXIC },
|
||||||
{ moveType: "Weather", move: MoveId.SANDSTORM },
|
{ moveType: "Weather", move: MoveId.SANDSTORM },
|
||||||
])("should not prevent fainting from $moveType", async ({ move }) => {
|
])("should not prevent fainting from $moveType Damage", async ({ move }) => {
|
||||||
game.override
|
game.override.moveset(move).enemyLevel(100);
|
||||||
.enemyLevel(1)
|
|
||||||
.startingLevel(100)
|
|
||||||
.enemySpecies(SpeciesId.MAGIKARP)
|
|
||||||
.moveset(move)
|
|
||||||
.enemyMoveset(MoveId.ENDURE);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.FEEBAS]);
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.FEEBAS]);
|
||||||
const enemy = game.field.getEnemyPokemon();
|
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);
|
game.move.select(move);
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
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 { allMoves } from "#data/data-lists";
|
||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
|
||||||
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 { MoveResult } from "#enums/move-result";
|
import { MoveResult } from "#enums/move-result";
|
||||||
|
import { MoveUseMode } from "#enums/move-use-mode";
|
||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
import { Stat } from "#enums/stat";
|
import { Stat } from "#enums/stat";
|
||||||
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, test, vi } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
describe("Moves - Protect", () => {
|
describe("Moves - Protect", () => {
|
||||||
let phaserGame: Phaser.Game;
|
let phaserGame: Phaser.Game;
|
||||||
@ -27,90 +26,210 @@ describe("Moves - Protect", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
game = new GameManager(phaserGame);
|
game = new GameManager(phaserGame);
|
||||||
|
|
||||||
game.override
|
game.override
|
||||||
.battleStyle("single")
|
.battleStyle("single")
|
||||||
.moveset([MoveId.PROTECT])
|
.moveset([MoveId.PROTECT, MoveId.SPIKY_SHIELD, MoveId.ENDURE, MoveId.SPLASH])
|
||||||
.enemySpecies(SpeciesId.SNORLAX)
|
.enemySpecies(SpeciesId.SNORLAX)
|
||||||
.enemyAbility(AbilityId.INSOMNIA)
|
.enemyAbility(AbilityId.INSOMNIA)
|
||||||
.enemyMoveset([MoveId.TACKLE])
|
.enemyMoveset(MoveId.LUMINA_CRASH)
|
||||||
.startingLevel(100)
|
.startingLevel(100)
|
||||||
.enemyLevel(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]);
|
await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
|
||||||
|
|
||||||
const leadPokemon = game.field.getPlayerPokemon();
|
const charizard = game.field.getPlayerPokemon();
|
||||||
|
|
||||||
game.move.select(MoveId.PROTECT);
|
game.move.select(MoveId.PROTECT);
|
||||||
|
|
||||||
await game.phaseInterceptor.to("BerryPhase", false);
|
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 () => {
|
it.each<{ numTurns: number; chance: number }>([
|
||||||
game.override.enemyMoveset([MoveId.CEASELESS_EDGE]);
|
{ numTurns: 1, chance: 3 },
|
||||||
vi.spyOn(allMoves[MoveId.CEASELESS_EDGE], "accuracy", "get").mockReturnValue(100);
|
{ 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]);
|
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);
|
game.move.select(MoveId.PROTECT);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
await game.phaseInterceptor.to("BerryPhase", false);
|
expect(charizard.hp).toBeLessThan(charizard.getMaxHp());
|
||||||
|
expect(charizard.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||||
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
|
expect(conditionSpy).toHaveLastReturnedWith(false);
|
||||||
expect(game.scene.arena.getTagOnSide(ArenaTrapTag, ArenaTagSide.ENEMY)).toBeUndefined();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should protect the user from status moves", async () => {
|
it("should share fail chance with all move variants", async () => {
|
||||||
game.override.enemyMoveset([MoveId.CHARM]);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
|
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);
|
game.move.select(MoveId.PROTECT);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
await game.phaseInterceptor.to("BerryPhase", false);
|
expect(charizard.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||||
|
|
||||||
expect(leadPokemon.getStatStage(Stat.ATK)).toBe(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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]);
|
game.override.enemyMoveset([MoveId.TACHYON_CUTTER]);
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
|
await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
|
||||||
|
|
||||||
const leadPokemon = game.field.getPlayerPokemon();
|
const charizard = game.field.getPlayerPokemon();
|
||||||
const enemyPokemon = game.field.getEnemyPokemon();
|
const enemyPokemon = game.field.getEnemyPokemon();
|
||||||
|
|
||||||
game.move.select(MoveId.PROTECT);
|
game.move.select(MoveId.PROTECT);
|
||||||
|
|
||||||
await game.phaseInterceptor.to("BerryPhase", false);
|
await game.phaseInterceptor.to("BerryPhase", false);
|
||||||
|
|
||||||
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
|
expect(charizard.hp).toBe(charizard.getMaxHp());
|
||||||
expect(enemyPokemon.turnData.hitCount).toBe(1);
|
expect(enemyPokemon.turnData.hitCount).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should fail if the user is the last to move in the turn", async () => {
|
it("should fail if the user moves last in the turn", async () => {
|
||||||
game.override.enemyMoveset([MoveId.PROTECT]);
|
game.override.enemyMoveset(MoveId.PROTECT);
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
|
await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
|
||||||
|
|
||||||
const leadPokemon = game.field.getPlayerPokemon();
|
const charizard = game.field.getPlayerPokemon();
|
||||||
const enemyPokemon = game.field.getEnemyPokemon();
|
const enemyPokemon = game.field.getEnemyPokemon();
|
||||||
|
|
||||||
game.move.select(MoveId.PROTECT);
|
game.move.select(MoveId.PROTECT);
|
||||||
|
|
||||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||||
|
|
||||||
await game.phaseInterceptor.to("BerryPhase", false);
|
await game.phaseInterceptor.to("BerryPhase", false);
|
||||||
|
|
||||||
expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
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 { MoveId } from "#enums/move-id";
|
||||||
import { MoveResult } from "#enums/move-result";
|
import { MoveResult } from "#enums/move-result";
|
||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
import { Stat } from "#enums/stat";
|
|
||||||
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, test } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
describe("Moves - Quick Guard", () => {
|
describe("Moves - Quick Guard", () => {
|
||||||
let phaserGame: Phaser.Game;
|
let phaserGame: Phaser.Game;
|
||||||
@ -27,74 +26,72 @@ describe("Moves - Quick Guard", () => {
|
|||||||
|
|
||||||
game.override
|
game.override
|
||||||
.battleStyle("double")
|
.battleStyle("double")
|
||||||
.moveset([MoveId.QUICK_GUARD, MoveId.SPLASH, MoveId.FOLLOW_ME])
|
.moveset([MoveId.QUICK_GUARD, MoveId.SPLASH, MoveId.SPIKY_SHIELD])
|
||||||
.enemySpecies(SpeciesId.SNORLAX)
|
.enemySpecies(SpeciesId.SNORLAX)
|
||||||
.enemyMoveset([MoveId.QUICK_ATTACK])
|
.enemyMoveset(MoveId.QUICK_ATTACK)
|
||||||
.enemyAbility(AbilityId.INSOMNIA)
|
.enemyAbility(AbilityId.BALL_FETCH)
|
||||||
.startingLevel(100)
|
.startingLevel(100)
|
||||||
.enemyLevel(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]);
|
await game.classicMode.startBattle([SpeciesId.CHARIZARD, SpeciesId.BLASTOISE]);
|
||||||
|
|
||||||
const playerPokemon = game.scene.getPlayerField();
|
const [charizard, blastoise] = game.scene.getPlayerField();
|
||||||
|
|
||||||
game.move.select(MoveId.QUICK_GUARD);
|
|
||||||
game.move.select(MoveId.SPLASH, 1);
|
|
||||||
|
|
||||||
|
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);
|
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 () => {
|
it.each<{ name: string; move: MoveId; ability: AbilityId }>([
|
||||||
game.override.enemyAbility(AbilityId.PRANKSTER).enemyMoveset([MoveId.GROWL]);
|
{ 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]);
|
await game.classicMode.startBattle([SpeciesId.CHARIZARD, SpeciesId.BLASTOISE]);
|
||||||
|
|
||||||
const playerPokemon = game.scene.getPlayerField();
|
const [charizard, blastoise] = game.scene.getPlayerField();
|
||||||
|
|
||||||
game.move.select(MoveId.QUICK_GUARD);
|
|
||||||
game.move.select(MoveId.SPLASH, 1);
|
|
||||||
|
|
||||||
|
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);
|
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 () => {
|
it("should increment (but not respect) other protection moves' fail counters", async () => {
|
||||||
game.override.enemyMoveset([MoveId.WATER_SHURIKEN]);
|
game.override.battleStyle("single");
|
||||||
|
|
||||||
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]);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
|
await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
|
||||||
|
|
||||||
const playerPokemon = game.field.getPlayerPokemon();
|
const charizard = game.scene.getPlayerPokemon()!;
|
||||||
const enemyPokemon = game.field.getEnemyPokemon();
|
// force protect to fail on anything >0 uses
|
||||||
|
vi.spyOn(charizard, "randBattleSeedInt").mockReturnValue(1);
|
||||||
|
|
||||||
game.move.select(MoveId.QUICK_GUARD);
|
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);
|
// ignored fail chance
|
||||||
expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
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 { AbilityId } from "#enums/ability-id";
|
||||||
|
import { BattlerIndex } from "#enums/battler-index";
|
||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
|
import { MoveResult } from "#enums/move-result";
|
||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
import { Stat } from "#enums/stat";
|
import { Stat } from "#enums/stat";
|
||||||
import { BerryPhase } from "#phases/berry-phase";
|
|
||||||
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, test } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
describe("Moves - Wide Guard", () => {
|
describe("Moves - Wide Guard", () => {
|
||||||
let phaserGame: Phaser.Game;
|
let phaserGame: Phaser.Game;
|
||||||
@ -26,71 +27,84 @@ describe("Moves - Wide Guard", () => {
|
|||||||
|
|
||||||
game.override
|
game.override
|
||||||
.battleStyle("double")
|
.battleStyle("double")
|
||||||
.moveset([MoveId.WIDE_GUARD, MoveId.SPLASH, MoveId.SURF])
|
.moveset([MoveId.WIDE_GUARD, MoveId.SPLASH, MoveId.SURF, MoveId.SPIKY_SHIELD])
|
||||||
.enemySpecies(SpeciesId.SNORLAX)
|
.enemySpecies(SpeciesId.SNORLAX)
|
||||||
.enemyMoveset(MoveId.SWIFT)
|
.enemyMoveset([MoveId.SWIFT, MoveId.GROWL, MoveId.TACKLE])
|
||||||
.enemyAbility(AbilityId.INSOMNIA)
|
.enemyAbility(AbilityId.INSOMNIA)
|
||||||
.startingLevel(100)
|
.startingLevel(100)
|
||||||
.enemyLevel(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]);
|
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);
|
expect(charizard.hp).toBe(charizard.getMaxHp());
|
||||||
game.move.select(MoveId.SPLASH, 1);
|
expect(blastoise.hp).toBe(blastoise.getMaxHp());
|
||||||
|
expect(charizard.getStatStage(Stat.ATK)).toBe(0);
|
||||||
await game.phaseInterceptor.to(BerryPhase, false);
|
expect(blastoise.getStatStage(Stat.ATK)).toBe(0);
|
||||||
|
|
||||||
leadPokemon.forEach(p => expect(p.hp).toBe(p.getMaxHp()));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should protect the user and allies from multi-target status moves", async () => {
|
it("should not protect the user and allies from single-target moves", async () => {
|
||||||
game.override.enemyMoveset([MoveId.GROWL]);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD, SpeciesId.BLASTOISE]);
|
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);
|
expect(charizard.hp).toBeLessThan(charizard.getMaxHp());
|
||||||
game.move.select(MoveId.SPLASH, 1);
|
expect(blastoise.hp).toBeLessThan(blastoise.getMaxHp());
|
||||||
|
|
||||||
await game.phaseInterceptor.to(BerryPhase, false);
|
|
||||||
|
|
||||||
leadPokemon.forEach(p => expect(p.getStatStage(Stat.ATK)).toBe(0));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should not protect the user and allies from single-target moves", async () => {
|
it("should protect the user from its ally's multi-target move", async () => {
|
||||||
game.override.enemyMoveset([MoveId.TACKLE]);
|
game.override.enemyMoveset(MoveId.SPLASH);
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD, SpeciesId.BLASTOISE]);
|
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.WIDE_GUARD, BattlerIndex.PLAYER);
|
||||||
game.move.select(MoveId.SPLASH, 1);
|
game.move.select(MoveId.SURF, BattlerIndex.PLAYER_2);
|
||||||
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
await game.phaseInterceptor.to(BerryPhase, false);
|
expect(charizard.hp).toBe(charizard.getMaxHp());
|
||||||
|
expect(snorlax1.hp).toBeLessThan(snorlax1.getMaxHp());
|
||||||
expect(leadPokemon.some(p => p.hp < p.getMaxHp())).toBeTruthy();
|
expect(snorlax2.hp).toBeLessThan(snorlax2.getMaxHp());
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should protect the user from its ally's multi-target move", async () => {
|
it("should increment (but not respect) other protection moves' fail counters", async () => {
|
||||||
game.override.enemyMoveset([MoveId.SPLASH]);
|
game.override.battleStyle("single");
|
||||||
|
await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD, SpeciesId.BLASTOISE]);
|
const charizard = game.scene.getPlayerPokemon()!;
|
||||||
|
// force protect to fail on anything other than a guaranteed success
|
||||||
const leadPokemon = game.scene.getPlayerField();
|
vi.spyOn(charizard, "randBattleSeedInt").mockReturnValue(1);
|
||||||
const enemyPokemon = game.scene.getEnemyField();
|
|
||||||
|
|
||||||
game.move.select(MoveId.WIDE_GUARD);
|
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());
|
// ignored fail chance
|
||||||
enemyPokemon.forEach(p => expect(p.hp).toBeLessThan(p.getMaxHp()));
|
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);
|
.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 () => {
|
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]);
|
await game.classicMode.startBattle([SpeciesId.ALOMOMOLA, SpeciesId.BLISSEY]);
|
||||||
|
|
||||||
@ -58,19 +49,19 @@ describe("Move - Wish", () => {
|
|||||||
game.move.use(MoveId.WISH);
|
game.move.use(MoveId.WISH);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
|
|
||||||
expectWishActive();
|
expect(game).toHavePositionalTag(PositionalTagType.WISH);
|
||||||
|
|
||||||
game.doSwitchPokemon(1);
|
game.doSwitchPokemon(1);
|
||||||
await game.toEndOfTurn();
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
expectWishActive(0);
|
expect(game).toHavePositionalTag(PositionalTagType.WISH, 0);
|
||||||
expect(game.textInterceptor.logs).toContain(
|
expect(game.textInterceptor.logs).toContain(
|
||||||
i18next.t("arenaTag:wishTagOnAdd", {
|
i18next.t("arenaTag:wishTagOnAdd", {
|
||||||
pokemonNameWithAffix: getPokemonNameWithAffix(alomomola),
|
pokemonNameWithAffix: getPokemonNameWithAffix(alomomola),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(alomomola.hp).toBe(1);
|
expect(alomomola).toHaveHp(1);
|
||||||
expect(blissey.hp).toBe(toDmgValue(alomomola.getMaxHp() / 2) + 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 () => {
|
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);
|
game.move.use(MoveId.WISH);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
|
|
||||||
expectWishActive();
|
expect(game).toHavePositionalTag(PositionalTagType.WISH);
|
||||||
|
|
||||||
game.move.use(MoveId.WISH);
|
game.move.use(MoveId.WISH);
|
||||||
await game.toEndOfTurn();
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
expect(alomomola.hp).toBe(toDmgValue(alomomola.getMaxHp() / 2) + 1);
|
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 () => {
|
it("should function independently of Future Sight", async () => {
|
||||||
@ -103,7 +94,8 @@ describe("Move - Wish", () => {
|
|||||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||||
await game.toNextTurn();
|
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 () => {
|
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.setTurnOrder(oldOrder.map(p => p.getBattlerIndex()));
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
|
|
||||||
expectWishActive(4);
|
expect(game).toHavePositionalTag(PositionalTagType.WISH, 4);
|
||||||
|
|
||||||
// Lower speed to change turn order
|
// Lower speed to change turn order
|
||||||
alomomola.setStatStage(Stat.SPD, 6);
|
alomomola.setStatStage(Stat.SPD, 6);
|
||||||
@ -141,7 +133,7 @@ describe("Move - Wish", () => {
|
|||||||
await game.phaseInterceptor.to("PositionalTagPhase");
|
await game.phaseInterceptor.to("PositionalTagPhase");
|
||||||
|
|
||||||
// all wishes have activated and added healing phases
|
// 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"));
|
const healPhases = game.scene.phaseManager.phaseQueue.filter(p => p.is("PokemonHealPhase"));
|
||||||
expect(healPhases).toHaveLength(4);
|
expect(healPhases).toHaveLength(4);
|
||||||
@ -165,14 +157,14 @@ describe("Move - Wish", () => {
|
|||||||
game.move.use(MoveId.WISH, BattlerIndex.PLAYER_2);
|
game.move.use(MoveId.WISH, BattlerIndex.PLAYER_2);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
|
|
||||||
expectWishActive();
|
expect(game).toHavePositionalTag(PositionalTagType.WISH);
|
||||||
|
|
||||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER);
|
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER);
|
||||||
game.move.use(MoveId.MEMENTO, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2);
|
game.move.use(MoveId.MEMENTO, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2);
|
||||||
await game.toEndOfTurn();
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
// Wish went away without doing anything
|
// Wish went away without doing anything
|
||||||
expectWishActive(0);
|
expect(game).toHavePositionalTag(PositionalTagType.WISH, 0);
|
||||||
expect(game.textInterceptor.logs).not.toContain(
|
expect(game.textInterceptor.logs).not.toContain(
|
||||||
i18next.t("arenaTag:wishTagOnAdd", {
|
i18next.t("arenaTag:wishTagOnAdd", {
|
||||||
pokemonNameWithAffix: getPokemonNameWithAffix(blissey),
|
pokemonNameWithAffix: getPokemonNameWithAffix(blissey),
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { getOnelineDiffStr } from "#test/test-utils/string-utils";
|
import { getOnelineDiffStr } from "#test/test-utils/string-utils";
|
||||||
|
import { receivedStr } from "#test/test-utils/test-utils";
|
||||||
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -14,22 +15,22 @@ export function toEqualArrayUnsorted(
|
|||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!Array.isArray(received)) {
|
if (!Array.isArray(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected an array, but got ${this.utils.stringify(received)}!`,
|
message: () => `Expected to receive an array, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (received.length !== expected.length) {
|
if (received.length !== expected.length) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: false,
|
||||||
message: () => `Expected to receive array of length ${received.length}, but got ${expected.length} instead!`,
|
message: () => `Expected to receive an array of length ${received.length}, but got ${expected.length} instead!`,
|
||||||
actual: received,
|
|
||||||
expected,
|
expected,
|
||||||
|
actual: received,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const actualSorted = received.slice().sort();
|
const actualSorted = received.toSorted();
|
||||||
const expectedSorted = expected.slice().sort();
|
const expectedSorted = expected.toSorted();
|
||||||
const pass = this.equals(actualSorted, expectedSorted, [...this.customTesters, this.utils.iterableEquality]);
|
const pass = this.equals(actualSorted, expectedSorted, [...this.customTesters, this.utils.iterableEquality]);
|
||||||
|
|
||||||
const actualStr = getOnelineDiffStr.call(this, actualSorted);
|
const actualStr = getOnelineDiffStr.call(this, actualSorted);
|
||||||
|
@ -21,7 +21,7 @@ export function toHaveAbilityApplied(
|
|||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to recieve a Pokemon, but got ${receivedStr(received)}!`,
|
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 { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||||
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||||
|
|
||||||
export interface ToHaveEffectiveStatMatcherOptions {
|
export interface toHaveEffectiveStatOptions {
|
||||||
/**
|
/**
|
||||||
* The target {@linkcode Pokemon}
|
* The target {@linkcode Pokemon}
|
||||||
* @see {@linkcode Pokemon.getEffectiveStat}
|
* @see {@linkcode Pokemon.getEffectiveStat}
|
||||||
@ -30,7 +30,7 @@ export interface ToHaveEffectiveStatMatcherOptions {
|
|||||||
* @param received - The object to check. Should be a {@linkcode Pokemon}
|
* @param received - The object to check. Should be a {@linkcode Pokemon}
|
||||||
* @param stat - The {@linkcode EffectiveStat} to check
|
* @param stat - The {@linkcode EffectiveStat} to check
|
||||||
* @param expectedValue - The expected value of the {@linkcode stat}
|
* @param expectedValue - The expected value of the {@linkcode stat}
|
||||||
* @param options - The {@linkcode ToHaveEffectiveStatMatcherOptions}
|
* @param options - The {@linkcode toHaveEffectiveStatOptions}
|
||||||
* @returns Whether the matcher passed
|
* @returns Whether the matcher passed
|
||||||
*/
|
*/
|
||||||
export function toHaveEffectiveStat(
|
export function toHaveEffectiveStat(
|
||||||
@ -38,11 +38,11 @@ export function toHaveEffectiveStat(
|
|||||||
received: unknown,
|
received: unknown,
|
||||||
stat: EffectiveStat,
|
stat: EffectiveStat,
|
||||||
expectedValue: number,
|
expectedValue: number,
|
||||||
{ enemy, move, isCritical = false }: ToHaveEffectiveStatMatcherOptions = {},
|
{ enemy, move, isCritical = false }: toHaveEffectiveStatOptions = {},
|
||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
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 {
|
export function toHaveFainted(this: MatcherState, received: unknown): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
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 {
|
export function toHaveFullHp(this: MatcherState, received: unknown): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
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 {
|
export function toHaveHp(this: MatcherState, received: unknown, expectedHp: number): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
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 {
|
): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expectedStage < -6 || expectedStage > 6) {
|
if (expectedStage < -6 || expectedStage > 6) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected ${expectedStage} to be within the range [-6, 6]!`,
|
message: () => `Expected ${expectedStage} to be within the range [-6, 6]!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ export function toHaveStatusEffect(
|
|||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
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;
|
const actualEffect = received.status?.effect ?? StatusEffect.NONE;
|
||||||
|
|
||||||
// Check exclusively effect equality first, coercing non-matching status effects to numbers.
|
// Check exclusively effect equality first, coercing non-matching status effects to numbers.
|
||||||
if (actualEffect !== (expectedStatus as Exclude<typeof expectedStatus, StatusEffect>)?.effect) {
|
if (typeof expectedStatus === "object" && actualEffect !== expectedStatus.effect) {
|
||||||
// This is actually 100% safe as `expectedStatus?.effect` will evaluate to `undefined` if a StatusEffect was passed,
|
expectedStatus = expectedStatus.effect;
|
||||||
// which will never match actualEffect by definition
|
|
||||||
expectedStatus = (expectedStatus as Exclude<typeof expectedStatus, StatusEffect>).effect;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof expectedStatus === "number") {
|
if (typeof expectedStatus === "number") {
|
||||||
|
@ -24,7 +24,7 @@ export function toHaveTakenDamage(
|
|||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -20,15 +20,15 @@ export function toHaveTerrain(
|
|||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!isGameManagerInstance(received)) {
|
if (!isGameManagerInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected GameManager, but got ${receivedStr(received)}!`,
|
message: () => `Expected to receive a GameManager, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!received.scene?.arena) {
|
if (!received.scene?.arena) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected GameManager.${received.scene ? "scene" : "scene.arena"} to be defined!`,
|
message: () => `Expected GameManager.${received.scene ? "scene.arena" : "scene"} to be defined!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,8 +41,8 @@ export function toHaveTerrain(
|
|||||||
pass,
|
pass,
|
||||||
message: () =>
|
message: () =>
|
||||||
pass
|
pass
|
||||||
? `Expected Arena to NOT have ${expectedStr} active, but it did!`
|
? `Expected the Arena to NOT have ${expectedStr} active, but it did!`
|
||||||
: `Expected Arena to have ${expectedStr} active, but got ${actualStr} instead!`,
|
: `Expected the Arena to have ${expectedStr} active, but got ${actualStr} instead!`,
|
||||||
expected: expectedTerrainType,
|
expected: expectedTerrainType,
|
||||||
actual,
|
actual,
|
||||||
};
|
};
|
||||||
|
@ -7,10 +7,16 @@ import { isPokemonInstance, receivedStr } from "../test-utils";
|
|||||||
|
|
||||||
export interface toHaveTypesOptions {
|
export interface toHaveTypesOptions {
|
||||||
/**
|
/**
|
||||||
* Whether to enforce exact matches (`true`) or superset matches (`false`).
|
* Value dictating the strength of the enforced typing match.
|
||||||
* @defaultValue `true`
|
*
|
||||||
|
* 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}.
|
* 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.
|
* Matcher that checks if a {@linkcode Pokemon}'s typing is as expected.
|
||||||
* @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 a {@linkcode Pokemon}
|
||||||
* @param options - The {@linkcode toHaveTypesOptions | options} for this matcher
|
* @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
|
* @returns The result of the matching
|
||||||
*/
|
*/
|
||||||
export function toHaveTypes(
|
export function toHaveTypes(
|
||||||
this: MatcherState,
|
this: MatcherState,
|
||||||
received: unknown,
|
received: unknown,
|
||||||
expected: [PokemonType, ...PokemonType[]],
|
expectedTypes: [PokemonType, ...PokemonType[]],
|
||||||
options: toHaveTypesOptions = {},
|
{ mode = "unordered", args = [] }: toHaveTypesOptions = {},
|
||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to recieve a Pokémon, but got ${receivedStr(received)}!`,
|
message: () => `Expected to recieve a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const actualTypes = received.getTypes(...(options.args ?? [])).sort();
|
// Return early if no types were passed in
|
||||||
const expectedTypes = expected.slice().sort();
|
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
|
// Exact matches do not care about subset equality
|
||||||
const matchers = options.exact
|
const matchers =
|
||||||
? [...this.customTesters, this.utils.iterableEquality]
|
mode === "superset"
|
||||||
: [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality];
|
? [...this.customTesters, this.utils.iterableEquality]
|
||||||
const pass = this.equals(actualTypes, expectedTypes, matchers);
|
: [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality];
|
||||||
|
const pass = this.equals(actualSorted, expectedSorted, matchers);
|
||||||
|
|
||||||
const actualStr = stringifyEnumArray(PokemonType, actualTypes);
|
const actualStr = stringifyEnumArray(PokemonType, actualSorted);
|
||||||
const expectedStr = stringifyEnumArray(PokemonType, expectedTypes);
|
const expectedStr = stringifyEnumArray(PokemonType, expectedSorted);
|
||||||
const pkmName = getPokemonNameWithAffix(received);
|
const pkmName = getPokemonNameWithAffix(received);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -55,7 +72,7 @@ export function toHaveTypes(
|
|||||||
pass
|
pass
|
||||||
? `Expected ${pkmName} to NOT have types ${expectedStr}, but it did!`
|
? `Expected ${pkmName} to NOT have types ${expectedStr}, but it did!`
|
||||||
: `Expected ${pkmName} to have types ${expectedStr}, but got ${actualStr} instead!`,
|
: `Expected ${pkmName} to have types ${expectedStr}, but got ${actualStr} instead!`,
|
||||||
expected: expectedTypes,
|
expected: expectedSorted,
|
||||||
actual: actualTypes,
|
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.
|
* Matcher to check the contents of a {@linkcode Pokemon}'s move history.
|
||||||
* @param received - The actual value received. Should be a {@linkcode Pokemon}
|
* @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
|
* 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.
|
* @param index - The index of the move history entry to check, in order from most recent to least recent.
|
||||||
* Default `0` (last used move)
|
* Default `0` (last used move)
|
||||||
@ -22,12 +22,12 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
|||||||
export function toHaveUsedMove(
|
export function toHaveUsedMove(
|
||||||
this: MatcherState,
|
this: MatcherState,
|
||||||
received: unknown,
|
received: unknown,
|
||||||
expectedResult: MoveId | AtLeastOne<TurnMove>,
|
expectedMove: MoveId | AtLeastOne<TurnMove>,
|
||||||
index = 0,
|
index = 0,
|
||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -37,34 +37,33 @@ export function toHaveUsedMove(
|
|||||||
|
|
||||||
if (move === undefined) {
|
if (move === undefined) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected ${pkmName} to have used ${index + 1} moves, but it didn't!`,
|
message: () => `Expected ${pkmName} to have used ${index + 1} moves, but it didn't!`,
|
||||||
actual: received.getLastXMoves(-1),
|
actual: received.getLastXMoves(-1),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Coerce to a `TurnMove`
|
// Coerce to a `TurnMove`
|
||||||
if (typeof expectedResult === "number") {
|
if (typeof expectedMove === "number") {
|
||||||
expectedResult = { move: expectedResult };
|
expectedMove = { move: expectedMove };
|
||||||
}
|
}
|
||||||
|
|
||||||
const moveIndexStr = index === 0 ? "last move" : `${getOrdinal(index)} most recent move`;
|
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.customTesters,
|
||||||
this.utils.subsetEquality,
|
this.utils.subsetEquality,
|
||||||
this.utils.iterableEquality,
|
this.utils.iterableEquality,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const expectedStr = getOnelineDiffStr.call(this, expectedResult);
|
const expectedStr = getOnelineDiffStr.call(this, expectedMove);
|
||||||
return {
|
return {
|
||||||
pass,
|
pass,
|
||||||
message: () =>
|
message: () =>
|
||||||
pass
|
pass
|
||||||
? `Expected ${pkmName}'s ${moveIndexStr} to NOT match ${expectedStr}, but it did!`
|
? `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 ${pkmName}'s ${moveIndexStr} to match ${expectedStr}, but it didn't!`,
|
expected: expectedMove,
|
||||||
expected: expectedResult,
|
|
||||||
actual: move,
|
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}.
|
* Matcher to check the amount of PP consumed by a {@linkcode Pokemon}.
|
||||||
* @param received - The actual value received. Should be 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,
|
* @param ppUsed - The numerical amount of PP that should have been consumed,
|
||||||
* or `all` to indicate the move should be _out_ of PP
|
* or `all` to indicate the move should be _out_ of PP
|
||||||
* @returns Whether the matcher passed
|
* @returns Whether the matcher passed
|
||||||
@ -23,12 +23,12 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
|||||||
export function toHaveUsedPP(
|
export function toHaveUsedPP(
|
||||||
this: MatcherState,
|
this: MatcherState,
|
||||||
received: unknown,
|
received: unknown,
|
||||||
expectedMove: MoveId,
|
moveId: MoveId,
|
||||||
ppUsed: number | "all",
|
ppUsed: number | "all",
|
||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
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;
|
const override = received.isPlayer() ? Overrides.MOVESET_OVERRIDE : Overrides.OPP_MOVESET_OVERRIDE;
|
||||||
if (coerceArray(override).length > 0) {
|
if (coerceArray(override).length > 0) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () =>
|
message: () =>
|
||||||
`Cannot test for PP consumption with ${received.isPlayer() ? "player" : "enemy"} moveset overrides active!`,
|
`Cannot test for PP consumption with ${received.isPlayer() ? "player" : "enemy"} moveset overrides active!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const pkmName = getPokemonNameWithAffix(received);
|
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) {
|
if (movesetMoves.length !== 1) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () =>
|
message: () =>
|
||||||
`Expected MoveId.${moveStr} to appear in ${pkmName}'s moveset exactly once, but got ${movesetMoves.length} times!`,
|
`Expected MoveId.${moveStr} to appear in ${pkmName}'s moveset exactly once, but got ${movesetMoves.length} times!`,
|
||||||
expected: expectedMove,
|
expected: moveId,
|
||||||
actual: received.getMoveset(),
|
actual: received.getMoveset(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -20,15 +20,15 @@ export function toHaveWeather(
|
|||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!isGameManagerInstance(received)) {
|
if (!isGameManagerInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected GameManager, but got ${receivedStr(received)}!`,
|
message: () => `Expected to receive a GameManager, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!received.scene?.arena) {
|
if (!received.scene?.arena) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected GameManager.${received.scene ? "scene" : "scene.arena"} to be defined!`,
|
message: () => `Expected GameManager.${received.scene ? "scene.arena" : "scene"} to be defined!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,8 +41,8 @@ export function toHaveWeather(
|
|||||||
pass,
|
pass,
|
||||||
message: () =>
|
message: () =>
|
||||||
pass
|
pass
|
||||||
? `Expected Arena to NOT have ${expectedStr} weather active, but it did!`
|
? `Expected the 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 have ${expectedStr} weather active, but got ${actualStr} instead!`,
|
||||||
expected: expectedWeatherType,
|
expected: expectedWeatherType,
|
||||||
actual,
|
actual,
|
||||||
};
|
};
|
||||||
|
@ -34,10 +34,10 @@ interface getEnumStrOptions {
|
|||||||
* @returns The stringified representation of `val` as dictated by the options.
|
* @returns The stringified representation of `val` as dictated by the options.
|
||||||
* @example
|
* @example
|
||||||
* ```ts
|
* ```ts
|
||||||
* enum fakeEnum {
|
* enum testEnum {
|
||||||
* ONE: 1,
|
* ONE = 1,
|
||||||
* TWO: 2,
|
* TWO = 2,
|
||||||
* THREE: 3,
|
* THREE = 3,
|
||||||
* }
|
* }
|
||||||
* getEnumStr(fakeEnum, fakeEnum.ONE); // Output: "ONE (=1)"
|
* getEnumStr(fakeEnum, fakeEnum.ONE); // Output: "ONE (=1)"
|
||||||
* getEnumStr(fakeEnum, fakeEnum.TWO, {casing: "Title", prefix: "fakeEnum.", suffix: "!!!"}); // Output: "fakeEnum.TWO!!! (=2)"
|
* 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.
|
* Convert an object into a oneline diff to be shown in an error message.
|
||||||
* @param obj - The object to return the oneline diff of
|
* @param obj - The object to return the oneline diff of
|
||||||
* @returns The updated diff
|
* @returns The updated diff
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const diff = getOnelineDiffStr.call(this, obj)
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
export function getOnelineDiffStr(this: MatcherState, obj: unknown): string {
|
export function getOnelineDiffStr(this: MatcherState, obj: unknown): string {
|
||||||
return this.utils
|
return this.utils
|
||||||
.stringify(obj, undefined, { maxLength: 35, indent: 0, printBasicPrototype: false })
|
.stringify(obj, undefined, { maxLength: 35, indent: 0, printBasicPrototype: false })
|
||||||
.replace(/\n/g, " ") // Replace newlines with spaces
|
.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