mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-07-04 15:32:18 +02:00
Moved utility functon to new file, fixed tests and such
This commit is contained in:
parent
09b828f4fb
commit
d4b2b0af26
@ -944,6 +944,7 @@ export default class BattleScene extends SceneBase {
|
||||
if (this.currentBattle.double === false) {
|
||||
return;
|
||||
}
|
||||
// TODO: Remove while loop
|
||||
if (allyPokemon?.isActive(true)) {
|
||||
let targetingMovePhase: MovePhase;
|
||||
do {
|
||||
|
@ -6947,12 +6947,10 @@ export function initAbilities() {
|
||||
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, Stat.DEF, 1),
|
||||
new Ability(Abilities.WIMP_OUT, 7)
|
||||
.attr(PostDamageForceSwitchAbAttr)
|
||||
.condition(getSheerForceHitDisableAbCondition())
|
||||
.bypassFaint(), // allows Wimp Out to activate with Reviver Seed
|
||||
.condition(getSheerForceHitDisableAbCondition()),
|
||||
new Ability(Abilities.EMERGENCY_EXIT, 7)
|
||||
.attr(PostDamageForceSwitchAbAttr)
|
||||
.condition(getSheerForceHitDisableAbCondition())
|
||||
.bypassFaint(),
|
||||
.condition(getSheerForceHitDisableAbCondition()),
|
||||
new Ability(Abilities.WATER_COMPACTION, 7)
|
||||
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => user.getMoveType(move) === PokemonType.WATER && move.category !== MoveCategory.STATUS, Stat.DEF, 2),
|
||||
new Ability(Abilities.MERCILESS, 7)
|
||||
|
@ -1823,8 +1823,8 @@ export class AddSubstituteAttr extends MoveEffectAttr {
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes 1/4 of the user's maximum HP (rounded down) to create a substitute for the user
|
||||
* @param user - The {@linkcode Pokemon} that used the move.
|
||||
* Removes a fraction of the user's maximum HP to create a substitute.
|
||||
* @param user - The {@linkcode Pokemon} using the move.
|
||||
* @param target - n/a
|
||||
* @param move - The {@linkcode Move} with this attribute.
|
||||
* @param args - n/a
|
||||
|
@ -1692,6 +1692,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
return this.getMaxHp() - this.hp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return this Pokemon's current HP as a fraction of its maximum HP.
|
||||
* @param precise - Whether to return the exact HP ratio (`true`) or rounded to the nearest 1% (`false`); default `false`
|
||||
* @returns This pokemon's current HP ratio (current / max).
|
||||
*/
|
||||
getHpRatio(precise = false): number {
|
||||
return precise ? this.hp / this.getMaxHp() : Math.round((this.hp / this.getMaxHp()) * 100) / 100;
|
||||
}
|
||||
@ -4048,15 +4053,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
isCritical = false,
|
||||
ignoreSegments = false,
|
||||
ignoreFaintPhase = false,
|
||||
}:
|
||||
{
|
||||
result?: DamageResult,
|
||||
isCritical?: boolean,
|
||||
ignoreSegments?: boolean,
|
||||
ignoreFaintPhase?: boolean,
|
||||
} = {}
|
||||
}: {
|
||||
result?: DamageResult;
|
||||
isCritical?: boolean;
|
||||
ignoreSegments?: boolean;
|
||||
ignoreFaintPhase?: boolean;
|
||||
} = {},
|
||||
): number {
|
||||
const isIndirectDamage = [ HitResult.INDIRECT, HitResult.INDIRECT_KO ].includes(result);
|
||||
const isIndirectDamage = [HitResult.INDIRECT, HitResult.INDIRECT_KO].includes(result);
|
||||
const damagePhase = new DamageAnimPhase(this.getBattlerIndex(), damage, result, isCritical);
|
||||
globalScene.unshiftPhase(damagePhase);
|
||||
|
||||
@ -4923,7 +4927,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
* which already calls this function.
|
||||
*/
|
||||
resetSummonData(): void {
|
||||
console.log(`resetSummonData called on Pokemon ${this.name}`)
|
||||
console.log(`resetSummonData called on Pokemon ${this.name}`);
|
||||
const illusion: IllusionData | null = this.summonData.illusion;
|
||||
if (this.summonData.speciesForm) {
|
||||
this.summonData.speciesForm = null;
|
||||
@ -4965,7 +4969,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
}
|
||||
|
||||
resetTurnData(): void {
|
||||
console.log(`resetTurnData called on Pokemon ${this.name}`)
|
||||
console.log(`resetTurnData called on Pokemon ${this.name}`);
|
||||
this.turnData = new PokemonTurnData();
|
||||
}
|
||||
|
||||
@ -5421,11 +5425,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
*/
|
||||
// TODO: Review where this is being called and where it is necessary to call it
|
||||
leaveField(clearEffects = true, hideInfo = true, destroy = false) {
|
||||
console.log(`leaveField called on Pokemon ${this.name}`)
|
||||
console.log(`leaveField called on Pokemon ${this.name}`);
|
||||
this.resetSprite();
|
||||
globalScene
|
||||
.getField(true)
|
||||
.filter(p => p !== this)
|
||||
.getField(true)
|
||||
.filter(p => p !== this)
|
||||
.forEach(p => p.removeTagsBySourceId(this.id));
|
||||
|
||||
if (clearEffects) {
|
||||
@ -6717,7 +6721,6 @@ export class EnemyPokemon extends Pokemon {
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Show or hide the type effectiveness multiplier window
|
||||
* Passing undefined will hide the window
|
||||
|
@ -30,9 +30,9 @@ import { SwitchPhase } from "./switch-phase";
|
||||
import { SwitchSummonPhase } from "./switch-summon-phase";
|
||||
import { ToggleDoublePositionPhase } from "./toggle-double-position-phase";
|
||||
import { VictoryPhase } from "./victory-phase";
|
||||
import { isNullOrUndefined } from "#app/utils/common";
|
||||
import { FRIENDSHIP_LOSS_FROM_FAINT } from "#app/data/balance/starters";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { isNullOrUndefined } from "#app/utils/common";
|
||||
|
||||
export class FaintPhase extends PokemonPhase {
|
||||
/**
|
||||
@ -118,6 +118,7 @@ export class FaintPhase extends PokemonPhase {
|
||||
|
||||
pokemon.resetTera();
|
||||
|
||||
// TODO: This could be simplified greatly with the concept of "move being used"
|
||||
if (pokemon.turnData.attacksReceived?.length) {
|
||||
const lastAttack = pokemon.turnData.attacksReceived[0];
|
||||
applyPostFaintAbAttrs(
|
||||
@ -151,41 +152,35 @@ export class FaintPhase extends PokemonPhase {
|
||||
}
|
||||
}
|
||||
|
||||
const legalBackupPokemon = globalScene.getBackupPartyMemberIndices(
|
||||
this.player,
|
||||
!this.player ? (pokemon as EnemyPokemon).trainerSlot : undefined,
|
||||
);
|
||||
|
||||
if (this.player) {
|
||||
/** The total number of Pokemon in the player's party that can legally fight */
|
||||
/** An array of Pokemon in the player's party that can legally fight. */
|
||||
const legalPlayerPokemon = globalScene.getPokemonAllowedInBattle();
|
||||
/** The total number of legal player Pokemon that aren't currently on the field */
|
||||
const legalPlayerPartyPokemon = legalPlayerPokemon.filter(p => !p.isActive(true));
|
||||
if (!legalPlayerPokemon.length) {
|
||||
/** If the player doesn't have any legal Pokemon, end the game */
|
||||
if (legalPlayerPokemon.length === 0) {
|
||||
// If the player doesn't have any legal Pokemon left in their party, end the game.
|
||||
globalScene.unshiftPhase(new GameOverPhase());
|
||||
} else if (
|
||||
globalScene.currentBattle.double &&
|
||||
legalPlayerPokemon.length === 1 &&
|
||||
legalPlayerPartyPokemon.length === 0
|
||||
) {
|
||||
/**
|
||||
* If the player has exactly one Pokemon in total at this point in a double battle, and that Pokemon
|
||||
* is already on the field, unshift a phase that moves that Pokemon to center position.
|
||||
*/
|
||||
} else if (globalScene.currentBattle.double && legalBackupPokemon.length === 0) {
|
||||
/*
|
||||
Otherwise, if the player has no reserve members left to switch in,
|
||||
unshift a phase to move the other on-field pokemon to center position.
|
||||
*/
|
||||
globalScene.unshiftPhase(new ToggleDoublePositionPhase(true));
|
||||
} else if (legalPlayerPartyPokemon.length > 0) {
|
||||
/**
|
||||
* If previous conditions weren't met, and the player has at least 1 legal Pokemon off the field,
|
||||
* push a phase that prompts the player to summon a Pokemon from their party.
|
||||
*/
|
||||
} else {
|
||||
// If previous conditions weren't met, push a phase to prompt the player to select a pokemon from their party.
|
||||
globalScene.pushPhase(new SwitchPhase(SwitchType.SWITCH, this.fieldIndex, true, false));
|
||||
}
|
||||
} else {
|
||||
// Unshift a phase for EXP gains and/or one to switch in a replacement party member.
|
||||
globalScene.unshiftPhase(new VictoryPhase(this.battlerIndex));
|
||||
if ([BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(globalScene.currentBattle.battleType)) {
|
||||
const hasReservePartyMember = !!globalScene
|
||||
.getEnemyParty()
|
||||
.filter(p => p.isActive() && !p.isOnField() && p.trainerSlot === (pokemon as EnemyPokemon).trainerSlot)
|
||||
.length;
|
||||
if (hasReservePartyMember) {
|
||||
globalScene.pushPhase(new SwitchSummonPhase(SwitchType.SWITCH, this.fieldIndex, -1, false, false));
|
||||
}
|
||||
if (
|
||||
[BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(globalScene.currentBattle.battleType) &&
|
||||
legalBackupPokemon.length > 0
|
||||
) {
|
||||
globalScene.pushPhase(new SwitchSummonPhase(SwitchType.SWITCH, this.fieldIndex, -1, false, false));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,7 +133,7 @@ export class SwitchSummonPhase extends SummonPhase {
|
||||
|
||||
// TODO: Why do we trigger these attributes even if the switch in target doesn't exist?
|
||||
// (This should almost certainly go somewhere inside `preSummon`)
|
||||
applyPreSummonAbAttrs(PreSummonAbAttr, switchedInPokemon);
|
||||
applyPreSummonAbAttrs(PreSummonAbAttr, switchInPokemon);
|
||||
applyPreSwitchOutAbAttrs(PreSwitchOutAbAttr, this.lastPokemon);
|
||||
if (!switchInPokemon) {
|
||||
this.end();
|
||||
|
53
src/utils/array.ts
Normal file
53
src/utils/array.ts
Normal file
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Split an array into a pair of arrays based on a conditional function.
|
||||
* @param array - The array to split into 2.
|
||||
* @param predicate - A function accepting up to 3 arguments. The split function calls the predicate function once per element of the array.
|
||||
* @param thisArg - An object to which the `this` keyword can refer in the predicate function. If omitted, `undefined` is used as the `this` value.
|
||||
* @returns A pair of shallowly-copied arrays containing every element for which `predicate` did or did not return a value coercible to the boolean `true`.
|
||||
* @overload
|
||||
*/
|
||||
export function splitArray<T, S extends T>(
|
||||
array: T[],
|
||||
predicate: (value: T, index: number, array: T[]) => value is S,
|
||||
thisArg?: unknown,
|
||||
): [matches: S[], nonMatches: S[]];
|
||||
|
||||
/**
|
||||
* Split an array into a pair of arrays based on a conditional function.
|
||||
* @param array - The array to split into 2.
|
||||
* @param predicate - A function accepting up to 3 arguments. The split function calls the function once per element of the array.
|
||||
* @param thisArg - An object to which the `this` keyword can refer in the predicate function. If omitted, `undefined` is used as the `this` value.
|
||||
* @returns A pair of shallowly-copied arrays containing every element for which `predicate` did or did not return a value coercible to the boolean `true`.
|
||||
* @overload
|
||||
*/
|
||||
export function splitArray<T>(
|
||||
array: T[],
|
||||
predicate: (value: T, index: number, array: T[]) => unknown,
|
||||
thisArg?: unknown,
|
||||
): [matches: T[], nonMatches: T[]];
|
||||
/**
|
||||
* Split an array into a pair of arrays based on a conditional function.
|
||||
* @param array - The array to split into 2.
|
||||
* @param predicate - A function accepting up to 3 arguments. The split function calls the function once per element of the array.
|
||||
* @param thisArg - An object to which the `this` keyword can refer in the predicate function. If omitted, `undefined` is used as the `this` value.
|
||||
* @returns A pair of shallowly-copied arrays containing every element for which `predicate` did or did not return a value coercible to the boolean `true`.
|
||||
* @overload
|
||||
*/
|
||||
export function splitArray<T>(
|
||||
array: T[],
|
||||
predicate: (value: T, index: number, array: T[]) => unknown,
|
||||
thisArg?: unknown,
|
||||
): [matches: T[], nonMatches: T[]] {
|
||||
const matches: T[] = [];
|
||||
const nonMatches: T[] = [];
|
||||
|
||||
const p = predicate.bind(thisArg) as typeof predicate;
|
||||
array.forEach((val, index, ar) => {
|
||||
if (p(val, index, ar)) {
|
||||
matches.push(val);
|
||||
} else {
|
||||
nonMatches.push(val);
|
||||
}
|
||||
});
|
||||
return [matches, nonMatches];
|
||||
}
|
@ -3,6 +3,7 @@ import { ArenaTagSide } from "#app/data/arena-tag";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||
import { BattleType } from "#enums/battle-type";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
@ -39,31 +40,28 @@ describe("Abilities - Mold Breaker", () => {
|
||||
game.override.startingLevel(100).enemyLevel(2).enemyAbility(Abilities.STURDY);
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
game.move.select(Moves.ERUPTION);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(game.scene.getEnemyPokemon()?.isFainted()).toBe(true);
|
||||
});
|
||||
|
||||
it("should turn off ignore abilities arena variable after the user's move concludes", async () => {
|
||||
game.override.startingLevel(100).enemyLevel(2);
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
|
||||
expect(globalScene.arena.ignoreAbilities).toBe(false);
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
expect(globalScene.arena.ignoreAbilities).toBe(true);
|
||||
expect(game.scene.arena.ignoreAbilities).toBe(true);
|
||||
expect(game.scene.arena.ignoringEffectSource).toBe(player.getBattlerIndex());
|
||||
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
expect(globalScene.arena.ignoreAbilities).toBe(false);
|
||||
expect(game.scene.arena.ignoreAbilities).toBe(false);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(game.scene.getEnemyPokemon()?.isFainted()).toBe(true);
|
||||
});
|
||||
|
||||
it("should keep Levitate opponents grounded when using force switch moves", async () => {
|
||||
game.override.enemyAbility(Abilities.LEVITATE).enemySpecies(Species.WEEZING).startingWave(8); // first rival battle; guaranteed 2 mon party
|
||||
game.override.enemyAbility(Abilities.LEVITATE).enemySpecies(Species.WEEZING).battleType(BattleType.TRAINER);
|
||||
|
||||
// Setup toxic spikes and stealth rock
|
||||
// Setup toxic spikes and spikes
|
||||
game.scene.arena.addTag(ArenaTagType.TOXIC_SPIKES, -1, Moves.TOXIC_SPIKES, 1, ArenaTagSide.ENEMY);
|
||||
game.scene.arena.addTag(ArenaTagType.SPIKES, -1, Moves.CEASELESS_EDGE, 1, ArenaTagSide.ENEMY);
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
@ -71,7 +69,7 @@ describe("Abilities - Mold Breaker", () => {
|
||||
const [weezing1, weezing2] = game.scene.getEnemyParty();
|
||||
// Weezing's levitate prevented removal of Toxic Spikes, ignored Spikes damage
|
||||
expect(game.scene.arena.getTagOnSide(ArenaTagType.TOXIC_SPIKES, ArenaTagSide.ENEMY)).toBeDefined();
|
||||
expect(weezing1.getHpRatio()).toBe(1);
|
||||
expect(weezing1.hp).toBe(weezing1.getMaxHp());
|
||||
|
||||
game.move.select(Moves.DRAGON_TAIL);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
@ -79,7 +77,7 @@ describe("Abilities - Mold Breaker", () => {
|
||||
// Levitate was ignored during the switch, causing Toxic Spikes to be removed and Spikes to deal damage
|
||||
expect(weezing1.isOnField()).toBe(false);
|
||||
expect(weezing2.isOnField()).toBe(true);
|
||||
expect(weezing2.getHpRatio()).toBeCloseTo(0.75);
|
||||
expect(weezing2.getHpRatio(true)).toBeCloseTo(0.75);
|
||||
expect(game.scene.arena.getTagOnSide(ArenaTagType.TOXIC_SPIKES, ArenaTagSide.ENEMY)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
@ -10,10 +10,14 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite
|
||||
import { BattleType } from "#enums/battle-type";
|
||||
import { TrainerSlot } from "#enums/trainer-slot";
|
||||
import { TrainerType } from "#enums/trainer-type";
|
||||
import { splitArray } from "#app/utils/common";
|
||||
import { splitArray } from "#app/utils/array";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { MoveResult } from "#app/field/pokemon";
|
||||
import { SubstituteTag } from "#app/data/battler-tags";
|
||||
import { Stat } from "#enums/stat";
|
||||
import i18next from "i18next";
|
||||
import { toDmgValue } from "#app/utils/common";
|
||||
import { allAbilities } from "#app/data/data-lists";
|
||||
|
||||
describe("Moves - Switching Moves", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -25,7 +29,7 @@ describe("Moves - Switching Moves", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Target Switch Moves", () => {
|
||||
describe("Force Switch Moves", () => {
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
@ -34,8 +38,8 @@ describe("Moves - Switching Moves", () => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.battleStyle("single")
|
||||
.ability(Abilities.NO_GUARD)
|
||||
.moveset([Moves.DRAGON_TAIL, Moves.SPLASH, Moves.FLAMETHROWER])
|
||||
.passiveAbility(Abilities.NO_GUARD)
|
||||
.moveset([Moves.DRAGON_TAIL, Moves.SPLASH, Moves.FLAMETHROWER, Moves.FOCUS_PUNCH])
|
||||
.enemySpecies(Species.WAILORD)
|
||||
.enemyMoveset(Moves.SPLASH);
|
||||
});
|
||||
@ -75,13 +79,15 @@ describe("Moves - Switching Moves", () => {
|
||||
it("should force trainers to switch randomly without selecting from a partner's party", async () => {
|
||||
game.override
|
||||
.battleStyle("double")
|
||||
.enemyMoveset(Moves.SPLASH)
|
||||
.enemyAbility(Abilities.STURDY)
|
||||
.battleType(BattleType.TRAINER)
|
||||
.randomTrainer({ trainerType: TrainerType.TATE, alwaysDouble: true })
|
||||
.enemySpecies(0);
|
||||
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRANITAR]);
|
||||
|
||||
expect(game.scene.currentBattle.trainer).not.toBeNull();
|
||||
const choiceSwitchSpy = vi.spyOn(game.scene.currentBattle.trainer!, "getNextSummonIndex");
|
||||
|
||||
// Grab each trainer's pokemon based on species name
|
||||
const [tateParty, lizaParty] = splitArray(
|
||||
game.scene.getEnemyParty(),
|
||||
@ -95,9 +101,6 @@ describe("Moves - Switching Moves", () => {
|
||||
// as Tate's pokemon are placed immediately before Liza's corresponding members.
|
||||
vi.fn(Phaser.Math.RND.integerInRange).mockImplementation(min => min);
|
||||
|
||||
// Spy on the function responsible for making informed switches
|
||||
const choiceSwitchSpy = vi.spyOn(game.scene.currentBattle.trainer!, "getNextSummonIndex");
|
||||
|
||||
game.move.select(Moves.DRAGON_TAIL, BattlerIndex.PLAYER, BattlerIndex.ENEMY_2);
|
||||
game.move.select(Moves.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
@ -114,29 +117,20 @@ describe("Moves - Switching Moves", () => {
|
||||
});
|
||||
|
||||
it("should force wild Pokemon to flee and redirect moves accordingly", async () => {
|
||||
game.override.battleStyle("double").enemyMoveset(Moves.SPLASH).enemyAbility(Abilities.ROUGH_SKIN);
|
||||
await game.classicMode.startBattle([Species.DRATINI, Species.DRATINI, Species.WAILORD, Species.WAILORD]);
|
||||
game.override.battleStyle("double").enemyMoveset(Moves.SPLASH);
|
||||
await game.classicMode.startBattle([Species.DRATINI, Species.DRATINI]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerParty()[0]!;
|
||||
const secPokemon = game.scene.getPlayerParty()[1]!;
|
||||
const [enemyLeadPokemon, enemySecPokemon] = game.scene.getEnemyParty();
|
||||
|
||||
const enemyLeadPokemon = game.scene.getEnemyParty()[0]!;
|
||||
const enemySecPokemon = game.scene.getEnemyParty()[1]!;
|
||||
|
||||
game.move.select(Moves.DRAGON_TAIL, 0, BattlerIndex.ENEMY);
|
||||
game.move.select(Moves.DRAGON_TAIL, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
|
||||
// target the same pokemon, second move should be redirected after first flees
|
||||
game.move.select(Moves.DRAGON_TAIL, 1, BattlerIndex.ENEMY);
|
||||
|
||||
// Focus punch used due to having even lower priority than Dtail
|
||||
game.move.select(Moves.FOCUS_PUNCH, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER, BattlerIndex.PLAYER_2]);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
const isVisibleLead = enemyLeadPokemon.visible;
|
||||
const hasFledLead = enemyLeadPokemon.switchOutStatus;
|
||||
const isVisibleSec = enemySecPokemon.visible;
|
||||
const hasFledSec = enemySecPokemon.switchOutStatus;
|
||||
expect(!isVisibleLead && hasFledLead && !isVisibleSec && hasFledSec).toBe(true);
|
||||
expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp());
|
||||
expect(secPokemon.hp).toBeLessThan(secPokemon.getMaxHp());
|
||||
expect(enemyLeadPokemon.hp).toBeLessThan(enemyLeadPokemon.getMaxHp());
|
||||
expect(enemyLeadPokemon.visible).toBe(false);
|
||||
expect(enemyLeadPokemon.switchOutStatus).toBe(true);
|
||||
expect(enemySecPokemon.hp).toBeLessThan(enemySecPokemon.getMaxHp());
|
||||
});
|
||||
|
||||
@ -153,7 +147,7 @@ describe("Moves - Switching Moves", () => {
|
||||
expect(enemy.isFullHp()).toBe(false);
|
||||
|
||||
// Turn 2: Mold Breaker should ignore switch blocking ability and switch out the target
|
||||
game.override.ability(Abilities.MOLD_BREAKER);
|
||||
vi.spyOn(game.scene.getPlayerPokemon()!, "getAbility").mockReturnValue(allAbilities[Abilities.MOLD_BREAKER]);
|
||||
enemy.hp = enemy.getMaxHp();
|
||||
|
||||
game.move.select(Moves.DRAGON_TAIL);
|
||||
@ -178,54 +172,53 @@ describe("Moves - Switching Moves", () => {
|
||||
expect(dondozo1.isFullHp()).toBe(false);
|
||||
});
|
||||
|
||||
it("should force a switch upon fainting an opponent normally", async () => {
|
||||
game.override.startingWave(5).startingLevel(1000); // To make sure Dragon Tail KO's the opponent
|
||||
it("should perform a normal switch upon fainting an opponent", async () => {
|
||||
game.override.battleType(BattleType.TRAINER).startingLevel(1000); // To make sure Dragon Tail KO's the opponent
|
||||
await game.classicMode.startBattle([Species.DRATINI]);
|
||||
|
||||
expect(game.scene.getEnemyParty()).toHaveLength(2);
|
||||
const choiceSwitchSpy = vi.spyOn(game.scene.currentBattle.trainer!, "getNextSummonIndex");
|
||||
game.move.select(Moves.DRAGON_TAIL);
|
||||
|
||||
await game.toNextTurn();
|
||||
|
||||
// Make sure the enemy switched to a healthy Pokemon
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
expect(enemy).toBeDefined();
|
||||
expect(enemy.isFullHp()).toBe(true);
|
||||
|
||||
// Make sure the enemy has a fainted Pokemon in their party and not on the field
|
||||
const faintedEnemy = game.scene.getEnemyParty().find(p => !p.isAllowedInBattle());
|
||||
expect(faintedEnemy).toBeDefined();
|
||||
expect(game.scene.getEnemyField().length).toBe(1);
|
||||
expect(choiceSwitchSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should neither switch nor softlock when activating an opponent's reviver seed", async () => {
|
||||
game.override
|
||||
.battleType(BattleType.TRAINER)
|
||||
.enemyHeldItems([{ name: "REVIVER_SEED" }])
|
||||
.startingLevel(1000); // make sure Dragon Tail KO's the opponent
|
||||
.enemySpecies(Species.BLISSEY)
|
||||
.enemyHeldItems([{ name: "REVIVER_SEED" }]);
|
||||
await game.classicMode.startBattle([Species.DRATINI]);
|
||||
|
||||
const [wailord1, wailord2] = game.scene.getEnemyParty()!;
|
||||
expect(wailord1).toBeDefined();
|
||||
expect(wailord2).toBeDefined();
|
||||
const [blissey1, blissey2] = game.scene.getEnemyParty()!;
|
||||
expect(blissey1).toBeDefined();
|
||||
expect(blissey2).toBeDefined();
|
||||
blissey1.hp = 1;
|
||||
|
||||
game.move.select(Moves.DRAGON_TAIL);
|
||||
await game.toNextTurn();
|
||||
|
||||
// Wailord should have consumed the reviver seed and stayed on field
|
||||
expect(wailord1.isOnField()).toBe(true);
|
||||
expect(wailord1.getHpRatio()).toBeCloseTo(0.5);
|
||||
expect(wailord1.getHeldItems()).toHaveLength(0);
|
||||
expect(wailord2.isOnField()).toBe(false);
|
||||
// Bliseey #1 should have consumed the reviver seed and stayed on field
|
||||
expect(blissey1.isOnField()).toBe(true);
|
||||
expect(blissey1.getHpRatio()).toBeCloseTo(0.5);
|
||||
expect(blissey1.getHeldItems()).toHaveLength(0);
|
||||
expect(blissey2.isOnField()).toBe(false);
|
||||
});
|
||||
|
||||
it("should neither switch nor softlock when activating a player's reviver seed", async () => {
|
||||
game.override
|
||||
.startingHeldItems([{ name: "REVIVER_SEED" }])
|
||||
.enemyMoveset(Moves.DRAGON_TAIL)
|
||||
.enemyLevel(1000); // make sure Dragon Tail KO's the player
|
||||
.startingLevel(1000); // make hp rounding consistent
|
||||
await game.classicMode.startBattle([Species.BLISSEY, Species.BULBASAUR]);
|
||||
|
||||
const [blissey, bulbasaur] = game.scene.getPlayerParty();
|
||||
blissey.hp = 1;
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
@ -279,6 +272,13 @@ describe("Moves - Switching Moves", () => {
|
||||
const newEnemy = game.scene.getEnemyPokemon()!;
|
||||
expect(newEnemy).not.toBe(enemy);
|
||||
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
|
||||
// TODO: Replace this with the locale key in question
|
||||
expect(game.textInterceptor.logs).toContain(
|
||||
i18next.t("INSERT FORCE SWITCH LOCALES KEY HERE", {
|
||||
pokemonName: newEnemy.getNameToRender(),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(game.textInterceptor.logs).not.toContain(
|
||||
i18next.t("battle:trainerGo", {
|
||||
trainerName: game.scene.currentBattle.trainer?.getName(newEnemy.trainerSlot),
|
||||
@ -338,225 +338,175 @@ describe("Moves - Switching Moves", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Failure Checks", () => {
|
||||
describe("Baton Pass", () => {
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override.battleStyle("single").enemySpecies(Species.GENGAR).disableCrits().enemyAbility(Abilities.STURDY);
|
||||
game.override
|
||||
.battleStyle("single")
|
||||
.enemySpecies(Species.MAGIKARP)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.moveset([Moves.BATON_PASS, Moves.NASTY_PLOT, Moves.SPLASH, Moves.SUBSTITUTE])
|
||||
.ability(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(Moves.SPLASH)
|
||||
.disableCrits();
|
||||
});
|
||||
|
||||
it.each<{ name: string; move: Moves }>([
|
||||
{ name: "U-Turn", move: Moves.U_TURN },
|
||||
{ name: "Flip Turn", move: Moves.FLIP_TURN },
|
||||
{ name: "Volt Switch", move: Moves.VOLT_SWITCH },
|
||||
{ name: "Baton Pass", move: Moves.BATON_PASS },
|
||||
{ name: "Shed Tail", move: Moves.SHED_TAIL },
|
||||
{ name: "Parting Shot", move: Moves.PARTING_SHOT },
|
||||
])("$name should not allow wild pokemon to flee", async ({ move }) => {
|
||||
game.override.moveset(Moves.SPLASH).enemyMoveset(move);
|
||||
it("should pass the user's stat stages and BattlerTags to an ally", async () => {
|
||||
await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]);
|
||||
|
||||
// reset species override so we get a different species
|
||||
game.override.enemySpecies(Species.ARBOK);
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
game.doSelectPartyPokemon(1);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
expect(player.species.speciesId).toBe(Species.SHUCKLE);
|
||||
expect(player.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
|
||||
expect(game.phaseInterceptor.log).not.toContain("BattleEndPhase");
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
expect(enemy.switchOutStatus).toBe(false);
|
||||
expect(enemy.species.speciesId).toBe(Species.GENGAR);
|
||||
});
|
||||
|
||||
it.each<{ name: string; move: Moves }>([
|
||||
{ name: "Teleport", move: Moves.TELEPORT },
|
||||
{ name: "Whirlwind", move: Moves.WHIRLWIND },
|
||||
{ name: "Roar", move: Moves.ROAR },
|
||||
{ name: "Dragon Tail", move: Moves.DRAGON_TAIL },
|
||||
{ name: "Circle Throw", move: Moves.CIRCLE_THROW },
|
||||
])("$name should allow wild pokemon to flee", async ({ move }) => {
|
||||
game.override.moveset(move).enemyMoveset(move);
|
||||
await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]);
|
||||
|
||||
const gengar = game.scene.getEnemyPokemon();
|
||||
game.move.select(move);
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
game.move.select(Moves.NASTY_PLOT);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(game.phaseInterceptor.log).not.toContain("BattleEndPhase");
|
||||
expect(game.scene.getEnemyPokemon()).toBe(gengar);
|
||||
const [raichu, shuckle] = game.scene.getPlayerParty();
|
||||
expect(raichu.getStatStage(Stat.SPATK)).toEqual(2);
|
||||
|
||||
game.move.select(Moves.SUBSTITUTE);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(raichu.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined();
|
||||
|
||||
game.move.select(Moves.BATON_PASS);
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(game.scene.getPlayerPokemon()).toBe(shuckle);
|
||||
expect(shuckle.getStatStage(Stat.SPATK)).toEqual(2);
|
||||
expect(shuckle.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined();
|
||||
});
|
||||
|
||||
it.each<{ name: string; move?: Moves; enemyMove?: Moves }>([
|
||||
{ name: "U-Turn", move: Moves.U_TURN },
|
||||
{ name: "Flip Turn", move: Moves.FLIP_TURN },
|
||||
{ name: "Volt Switch", move: Moves.VOLT_SWITCH },
|
||||
// TODO: Enable once Parting shot is fixed
|
||||
// {name: "Parting Shot", move: Moves.PARTING_SHOT},
|
||||
{ name: "Dragon Tail", enemyMove: Moves.DRAGON_TAIL },
|
||||
{ name: "Circle Throw", enemyMove: Moves.CIRCLE_THROW },
|
||||
])(
|
||||
"$name should not fail if no valid switch out target is found",
|
||||
async ({ move = Moves.SPLASH, enemyMove = Moves.SPLASH }) => {
|
||||
game.override.moveset(move).enemyMoveset(enemyMove);
|
||||
await game.classicMode.startBattle([Species.RAICHU]);
|
||||
it("should pass stat stages when used by enemy trainers", async () => {
|
||||
game.override.battleType(BattleType.TRAINER).enemyMoveset([Moves.NASTY_PLOT, Moves.BATON_PASS]);
|
||||
await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]);
|
||||
|
||||
game.move.select(move);
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.toNextTurn();
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
|
||||
expect(game.scene.getPlayerPokemon()?.getLastXMoves()[0].result).toBe(MoveResult.MISS);
|
||||
},
|
||||
);
|
||||
// round 1 - ai buffs
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.NASTY_PLOT);
|
||||
await game.toNextTurn();
|
||||
|
||||
it.each<{ name: string; move?: Moves; enemyMove?: Moves }>([
|
||||
{ name: "Teleport", move: Moves.TELEPORT },
|
||||
{ name: "Baton Pass", move: Moves.BATON_PASS },
|
||||
{ name: "Shed Tail", move: Moves.SHED_TAIL },
|
||||
{ name: "Roar", enemyMove: Moves.ROAR },
|
||||
{ name: "Whirlwind", enemyMove: Moves.WHIRLWIND },
|
||||
])(
|
||||
"$name should fail if no valid switch out target is found",
|
||||
async ({ move = Moves.SPLASH, enemyMove = Moves.SPLASH }) => {
|
||||
game.override.moveset(move).enemyMoveset(enemyMove);
|
||||
await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]);
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.BATON_PASS);
|
||||
await game.toNextTurn();
|
||||
|
||||
// reset species override so we get a different species
|
||||
game.override.enemySpecies(Species.ARBOK);
|
||||
// check buffs are still there
|
||||
const newEnemy = game.scene.getEnemyPokemon()!;
|
||||
expect(newEnemy).not.toBe(enemy);
|
||||
expect(newEnemy.getStatStage(Stat.SPATK)).toBe(2);
|
||||
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
|
||||
});
|
||||
|
||||
game.move.select(move);
|
||||
game.doSelectPartyPokemon(1);
|
||||
it("should not transfer non-transferrable effects", async () => {
|
||||
game.override.enemyMoveset([Moves.SALT_CURE]);
|
||||
await game.classicMode.startBattle([Species.PIKACHU, Species.FEEBAS]);
|
||||
|
||||
await game.toNextTurn();
|
||||
const [player1, player2] = game.scene.getPlayerParty();
|
||||
|
||||
expect(game.phaseInterceptor.log).not.toContain("BattleEndPhase");
|
||||
expect(game.scene.getEnemyPokemon()!.species.speciesId).toBe(Species.GENGAR);
|
||||
},
|
||||
);
|
||||
game.move.select(Moves.BATON_PASS);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
|
||||
describe("Baton Pass", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
// enemy salt cure
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
expect(player1.getTag(BattlerTagType.SALT_CURED)).toBeDefined();
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.toNextTurn();
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
expect(player1.isOnField()).toBe(false);
|
||||
expect(player2.isOnField()).toBe(true);
|
||||
expect(player2.getTag(BattlerTagType.SALT_CURED)).toBeUndefined();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.battleStyle("single")
|
||||
.enemySpecies(Species.MAGIKARP)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.moveset([Moves.BATON_PASS, Moves.NASTY_PLOT, Moves.SPLASH, Moves.SUBSTITUTE])
|
||||
.ability(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(Moves.SPLASH)
|
||||
.disableCrits();
|
||||
});
|
||||
it("should remove the user's binding effects", async () => {
|
||||
game.override.moveset([Moves.FIRE_SPIN, Moves.BATON_PASS]);
|
||||
|
||||
it("should pass the user's stat stages and BattlerTags to an ally", async () => {
|
||||
await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]);
|
||||
await game.classicMode.startBattle([Species.MAGIKARP, Species.FEEBAS]);
|
||||
|
||||
game.move.select(Moves.NASTY_PLOT);
|
||||
await game.toNextTurn();
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
const [raichu, shuckle] = game.scene.getPlayerParty();
|
||||
expect(raichu.getStatStage(Stat.SPATK)).toEqual(2);
|
||||
game.move.select(Moves.FIRE_SPIN);
|
||||
await game.move.forceHit();
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(Moves.SUBSTITUTE);
|
||||
await game.toNextTurn();
|
||||
expect(enemy.getTag(BattlerTagType.FIRE_SPIN)).toBeDefined();
|
||||
|
||||
expect(raichu.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined();
|
||||
game.move.select(Moves.BATON_PASS);
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(Moves.BATON_PASS);
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(enemy.getTag(BattlerTagType.FIRE_SPIN)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
expect(game.scene.getPlayerPokemon()).toBe(shuckle);
|
||||
expect(shuckle.getStatStage(Stat.SPATK)).toEqual(2);
|
||||
expect(shuckle.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined();
|
||||
});
|
||||
describe("Shed Tail", () => {
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
it("should pass stat stages when used by enemy trainers", async () => {
|
||||
game.override.battleType(BattleType.TRAINER).enemyMoveset([Moves.NASTY_PLOT, Moves.BATON_PASS]);
|
||||
await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]);
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.moveset(Moves.SHED_TAIL)
|
||||
.battleStyle("single")
|
||||
.enemySpecies(Species.SNORLAX)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(Moves.SPLASH);
|
||||
});
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
it("should consume 50% of the user's max HP (rounded up) to transfer a 25% HP Substitute doll", async () => {
|
||||
await game.classicMode.startBattle([Species.MAGIKARP, Species.FEEBAS]);
|
||||
|
||||
// round 1 - ai buffs
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.NASTY_PLOT);
|
||||
await game.toNextTurn();
|
||||
const magikarp = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.BATON_PASS);
|
||||
await game.toNextTurn();
|
||||
game.move.select(Moves.SHED_TAIL);
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.phaseInterceptor.to("TurnEndPhase", false);
|
||||
|
||||
// check buffs are still there
|
||||
const newEnemy = game.scene.getEnemyPokemon()!;
|
||||
expect(newEnemy).not.toBe(enemy);
|
||||
expect(newEnemy.getStatStage(Stat.SPATK)).toBe(2);
|
||||
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
|
||||
});
|
||||
const feebas = game.scene.getPlayerPokemon()!;
|
||||
expect(feebas).not.toBe(magikarp);
|
||||
expect(feebas.hp).toBe(feebas.getMaxHp());
|
||||
|
||||
it("should not transfer non-transferrable effects", async () => {
|
||||
game.override.enemyMoveset([Moves.SALT_CURE]);
|
||||
await game.classicMode.startBattle([Species.PIKACHU, Species.FEEBAS]);
|
||||
const substituteTag = feebas.getTag(SubstituteTag)!;
|
||||
expect(substituteTag).toBeDefined();
|
||||
|
||||
const [player1, player2] = game.scene.getPlayerParty();
|
||||
// Note: Altered the test to be consistent with the correct HP cost :yipeevee_static:
|
||||
expect(magikarp.getInverseHp()).toBe(Math.ceil(magikarp.getMaxHp() / 2));
|
||||
expect(substituteTag.hp).toBe(Math.floor(magikarp.getMaxHp() / 4));
|
||||
});
|
||||
|
||||
game.move.select(Moves.BATON_PASS);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
it("should not transfer other effects", async () => {
|
||||
await game.classicMode.startBattle([Species.MAGIKARP, Species.FEEBAS]);
|
||||
|
||||
// enemy salt cure
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
expect(player1.getTag(BattlerTagType.SALT_CURED)).toBeDefined();
|
||||
const magikarp = game.scene.getPlayerPokemon()!;
|
||||
magikarp.setStatStage(Stat.ATK, 6);
|
||||
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.toNextTurn();
|
||||
game.move.select(Moves.SHED_TAIL);
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.phaseInterceptor.to("TurnEndPhase", false);
|
||||
|
||||
expect(player1.isOnField()).toBe(false);
|
||||
expect(player2.isOnField()).toBe(true);
|
||||
expect(player2.getTag(BattlerTagType.SALT_CURED)).toBeUndefined();
|
||||
});
|
||||
const feebas = game.scene.getPlayerPokemon()!;
|
||||
expect(feebas).not.toBe(magikarp);
|
||||
expect(feebas.getStatStage(Stat.ATK)).toBe(0);
|
||||
expect(magikarp.getStatStage(Stat.ATK)).toBe(0);
|
||||
});
|
||||
|
||||
it("removes the user's binding effects", async () => {
|
||||
game.override.moveset([Moves.FIRE_SPIN, Moves.BATON_PASS]);
|
||||
it("should fail if the user's HP is insufficient", async () => {
|
||||
await game.classicMode.startBattle([Species.MAGIKARP, Species.FEEBAS]);
|
||||
|
||||
await game.classicMode.startBattle([Species.MAGIKARP, Species.FEEBAS]);
|
||||
const magikarp = game.scene.getPlayerPokemon()!;
|
||||
const initHp = toDmgValue(magikarp.getMaxHp() / 2 - 1);
|
||||
magikarp.hp = initHp;
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
game.move.select(Moves.SHED_TAIL);
|
||||
await game.phaseInterceptor.to("TurnEndPhase", false);
|
||||
|
||||
game.move.select(Moves.FIRE_SPIN);
|
||||
await game.move.forceHit();
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemy.getTag(BattlerTagType.FIRE_SPIN)).toBeDefined();
|
||||
|
||||
game.move.select(Moves.BATON_PASS);
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemy.getTag(BattlerTagType.FIRE_SPIN)).toBeUndefined();
|
||||
});
|
||||
expect(magikarp.isOnField()).toBe(true);
|
||||
expect(magikarp.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
expect(magikarp.hp).toBe(initHp);
|
||||
});
|
||||
});
|
||||
|
||||
@ -664,7 +614,7 @@ describe("Moves - Switching Moves", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Shed Tail", () => {
|
||||
describe("Failure Checks", () => {
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
@ -672,47 +622,97 @@ describe("Moves - Switching Moves", () => {
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.moveset(Moves.SHED_TAIL)
|
||||
.battleStyle("single")
|
||||
.enemySpecies(Species.SNORLAX)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(Moves.SPLASH);
|
||||
.passiveAbility(Abilities.NO_GUARD)
|
||||
.enemySpecies(Species.GENGAR)
|
||||
.disableCrits()
|
||||
.enemyAbility(Abilities.STURDY);
|
||||
});
|
||||
|
||||
it("should consume 50% of the user's max HP (rounded up) to transfer a 25% HP Substitute doll", async () => {
|
||||
await game.classicMode.startBattle([Species.MAGIKARP, Species.FEEBAS]);
|
||||
it.each<{ name: string; move: Moves }>([
|
||||
{ name: "U-Turn", move: Moves.U_TURN },
|
||||
{ name: "Flip Turn", move: Moves.FLIP_TURN },
|
||||
{ name: "Volt Switch", move: Moves.VOLT_SWITCH },
|
||||
{ name: "Baton Pass", move: Moves.BATON_PASS },
|
||||
{ name: "Shed Tail", move: Moves.SHED_TAIL },
|
||||
{ name: "Parting Shot", move: Moves.PARTING_SHOT },
|
||||
])("$name should not allow wild pokemon to flee", async ({ move }) => {
|
||||
game.override.moveset(Moves.SPLASH).enemyMoveset(move);
|
||||
await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]);
|
||||
|
||||
const magikarp = game.scene.getPlayerPokemon()!;
|
||||
const gengar = game.scene.getEnemyPokemon()!;
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
game.move.select(Moves.SHED_TAIL);
|
||||
expect(game.phaseInterceptor.log).not.toContain("BattleEndPhase");
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
expect(enemy).toBe(gengar);
|
||||
expect(enemy.switchOutStatus).toBe(false);
|
||||
});
|
||||
|
||||
it.each<{ name: string; move?: Moves; enemyMove?: Moves }>([
|
||||
{ name: "Teleport", enemyMove: Moves.TELEPORT },
|
||||
{ name: "Whirlwind", move: Moves.WHIRLWIND },
|
||||
{ name: "Roar", move: Moves.ROAR },
|
||||
{ name: "Dragon Tail", move: Moves.DRAGON_TAIL },
|
||||
{ name: "Circle Throw", move: Moves.CIRCLE_THROW },
|
||||
])("$name should allow wild pokemon to flee", async ({ move = Moves.SPLASH, enemyMove = Moves.SPLASH }) => {
|
||||
game.override.moveset(move).enemyMoveset(enemyMove);
|
||||
await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]);
|
||||
|
||||
const gengar = game.scene.getEnemyPokemon();
|
||||
game.move.select(move);
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.toNextTurn();
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase", false);
|
||||
|
||||
const feebas = game.scene.getPlayerPokemon()!;
|
||||
expect(feebas).not.toBe(magikarp);
|
||||
expect(feebas.hp).toBe(feebas.getMaxHp());
|
||||
|
||||
const substituteTag = feebas.getTag(SubstituteTag)!;
|
||||
expect(substituteTag).toBeDefined();
|
||||
|
||||
// Note: Altered the test to be consistent with the correct HP cost :yipeevee_static:
|
||||
expect(magikarp.getInverseHp()).toBe(Math.ceil(magikarp.getMaxHp() / 2));
|
||||
expect(substituteTag.hp).toBe(Math.ceil(magikarp.getMaxHp() / 4));
|
||||
expect(game.phaseInterceptor.log).toContain("BattleEndPhase");
|
||||
expect(game.scene.getEnemyPokemon()).not.toBe(gengar);
|
||||
});
|
||||
|
||||
it("should fail if user's HP is insufficient", async () => {
|
||||
await game.classicMode.startBattle([Species.MAGIKARP, Species.FEEBAS]);
|
||||
it.each<{ name: string; move?: Moves; enemyMove?: Moves }>([
|
||||
{ name: "U-Turn", move: Moves.U_TURN },
|
||||
{ name: "Flip Turn", move: Moves.FLIP_TURN },
|
||||
{ name: "Volt Switch", move: Moves.VOLT_SWITCH },
|
||||
// TODO: Enable once Parting shot is fixed
|
||||
// { name: "Parting Shot", move: Moves.PARTING_SHOT },
|
||||
{ name: "Dragon Tail", enemyMove: Moves.DRAGON_TAIL },
|
||||
{ name: "Circle Throw", enemyMove: Moves.CIRCLE_THROW },
|
||||
])(
|
||||
"$name should not fail if no valid switch out target is found",
|
||||
async ({ move = Moves.SPLASH, enemyMove = Moves.SPLASH }) => {
|
||||
game.override.moveset(move).enemyMoveset(enemyMove);
|
||||
await game.classicMode.startBattle([Species.RAICHU]);
|
||||
|
||||
const magikarp = game.scene.getPlayerPokemon()!;
|
||||
magikarp.hp = Math.floor(magikarp.getMaxHp() / 2 - 1);
|
||||
game.move.select(move);
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(Moves.SHED_TAIL);
|
||||
await game.phaseInterceptor.to("TurnEndPhase", false);
|
||||
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
|
||||
const user = enemyMove === Moves.SPLASH ? game.scene.getPlayerPokemon()! : game.scene.getEnemyPokemon()!;
|
||||
expect(user.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
},
|
||||
);
|
||||
|
||||
expect(magikarp.isOnField()).toBe(true);
|
||||
expect(magikarp.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
expect(magikarp.hp).toBe(magikarp.getMaxHp() / 2 - 1);
|
||||
});
|
||||
it.each<{ name: string; move?: Moves; enemyMove?: Moves }>([
|
||||
{ name: "Teleport", move: Moves.TELEPORT },
|
||||
{ name: "Baton Pass", move: Moves.BATON_PASS },
|
||||
{ name: "Shed Tail", move: Moves.SHED_TAIL },
|
||||
{ name: "Roar", enemyMove: Moves.ROAR },
|
||||
{ name: "Whirlwind", enemyMove: Moves.WHIRLWIND },
|
||||
])(
|
||||
"$name should fail if no valid switch out target is found",
|
||||
async ({ move = Moves.SPLASH, enemyMove = Moves.SPLASH }) => {
|
||||
game.override.moveset(move).enemyMoveset(enemyMove);
|
||||
await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]);
|
||||
|
||||
game.move.select(move);
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
|
||||
const user = enemyMove === Moves.SPLASH ? game.scene.getPlayerPokemon()! : game.scene.getEnemyPokemon()!;
|
||||
expect(user.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user