[Test] Add forceEnemyMove Game Manager util (#3678)

* Add `forceEnemyMove` test util

* fix ceaseless edge test

* Apply flx's suggestions

Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>

* Rewrite Follow Me test

* Reorganize new imports in game manager

* Rewrite Rage Powder + Spotlight tests

---------

Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>
This commit is contained in:
innerthunder 2024-09-04 10:56:57 -07:00 committed by GitHub
parent 11ac929a4d
commit 207b3e1eb7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 98 additions and 63 deletions

View File

@ -77,4 +77,8 @@ export class EnemyCommandPhase extends FieldPhase {
this.end();
}
getFieldIndex(): number {
return this.fieldIndex;
}
}

View File

@ -110,7 +110,7 @@ describe("Moves - Ceaseless Edge", () => {
const hpBeforeSpikes = game.scene.currentBattle.enemyParty[1].hp;
// Check HP of pokemon that WILL BE switched in (index 1)
game.forceOpponentToSwitch();
game.forceEnemyToSwitch();
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to(TurnEndPhase, false);
expect(game.scene.currentBattle.enemyParty[0].hp).toBeLessThan(hpBeforeSpikes);

View File

@ -123,7 +123,7 @@ describe("Moves - Focus Punch", () => {
await game.startBattle([Species.CHARIZARD]);
game.forceOpponentToSwitch();
game.forceEnemyToSwitch();
game.move.select(Moves.FOCUS_PUNCH);
await game.phaseInterceptor.to(TurnStartPhase);

View File

@ -28,48 +28,55 @@ describe("Moves - Follow Me", () => {
game = new GameManager(phaserGame);
game.override.battleType("double");
game.override.starterSpecies(Species.AMOONGUSS);
game.override.ability(Abilities.BALL_FETCH);
game.override.enemySpecies(Species.SNORLAX);
game.override.startingLevel(100);
game.override.enemyLevel(100);
game.override.moveset([Moves.FOLLOW_ME, Moves.RAGE_POWDER, Moves.SPOTLIGHT, Moves.QUICK_ATTACK]);
game.override.enemyMoveset([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]);
game.override.enemyMoveset([Moves.TACKLE, Moves.FOLLOW_ME, Moves.SPLASH]);
});
test(
"move should redirect enemy attacks to the user",
async () => {
await game.startBattle([Species.AMOONGUSS, Species.CHARIZARD]);
await game.classicMode.startBattle([Species.AMOONGUSS, Species.CHARIZARD]);
const playerPokemon = game.scene.getPlayerField();
const playerStartingHp = playerPokemon.map(p => p.hp);
game.move.select(Moves.FOLLOW_ME);
game.move.select(Moves.QUICK_ATTACK, 1, BattlerIndex.ENEMY);
// Force both enemies to target the player Pokemon that did not use Follow Me
await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER_2);
await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER_2);
await game.phaseInterceptor.to(TurnEndPhase, false);
expect(playerPokemon[0].hp).toBeLessThan(playerStartingHp[0]);
expect(playerPokemon[1].hp).toBe(playerStartingHp[1]);
expect(playerPokemon[0].hp).toBeLessThan(playerPokemon[0].getMaxHp());
expect(playerPokemon[1].hp).toBe(playerPokemon[1].getMaxHp());
}, TIMEOUT
);
test(
"move should redirect enemy attacks to the first ally that uses it",
async () => {
await game.startBattle([Species.AMOONGUSS, Species.CHARIZARD]);
await game.classicMode.startBattle([Species.AMOONGUSS, Species.CHARIZARD]);
const playerPokemon = game.scene.getPlayerField();
const playerStartingHp = playerPokemon.map(p => p.hp);
game.move.select(Moves.FOLLOW_ME);
game.move.select(Moves.FOLLOW_ME, 1);
// Each player is targeted by an enemy
await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER);
await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER_2);
await game.phaseInterceptor.to(TurnEndPhase, false);
playerPokemon.sort((a, b) => a.getEffectiveStat(Stat.SPD) - b.getEffectiveStat(Stat.SPD));
expect(playerPokemon[1].hp).toBeLessThan(playerStartingHp[1]);
expect(playerPokemon[0].hp).toBe(playerStartingHp[0]);
expect(playerPokemon[1].hp).toBeLessThan(playerPokemon[1].getMaxHp());
expect(playerPokemon[0].hp).toBe(playerPokemon[0].getMaxHp());
}, TIMEOUT
);
@ -78,21 +85,23 @@ describe("Moves - Follow Me", () => {
async () => {
game.override.ability(Abilities.STALWART);
game.override.moveset([Moves.QUICK_ATTACK]);
game.override.enemyMoveset([Moves.FOLLOW_ME, Moves.FOLLOW_ME, Moves.FOLLOW_ME, Moves.FOLLOW_ME]);
await game.startBattle([Species.AMOONGUSS, Species.CHARIZARD]);
await game.classicMode.startBattle([Species.AMOONGUSS, Species.CHARIZARD]);
const enemyPokemon = game.scene.getEnemyField();
const enemyStartingHp = enemyPokemon.map(p => p.hp);
game.move.select(Moves.QUICK_ATTACK, 0, BattlerIndex.ENEMY);
game.move.select(Moves.QUICK_ATTACK, 1, BattlerIndex.ENEMY_2);
// Target doesn't need to be specified if the move is self-targeted
await game.forceEnemyMove(Moves.FOLLOW_ME);
await game.forceEnemyMove(Moves.SPLASH);
await game.phaseInterceptor.to(TurnEndPhase, false);
// If redirection was bypassed, both enemies should be damaged
expect(enemyPokemon[0].hp).toBeLessThan(enemyStartingHp[0]);
expect(enemyPokemon[1].hp).toBeLessThan(enemyStartingHp[1]);
expect(enemyPokemon[0].hp).toBeLessThan(enemyPokemon[0].getMaxHp());
expect(enemyPokemon[1].hp).toBeLessThan(enemyPokemon[1].getMaxHp());
}, TIMEOUT
);
@ -100,21 +109,22 @@ describe("Moves - Follow Me", () => {
"move effect should be bypassed by Snipe Shot",
async () => {
game.override.moveset([Moves.SNIPE_SHOT]);
game.override.enemyMoveset([Moves.FOLLOW_ME, Moves.FOLLOW_ME, Moves.FOLLOW_ME, Moves.FOLLOW_ME]);
await game.startBattle([Species.AMOONGUSS, Species.CHARIZARD]);
await game.classicMode.startBattle([Species.AMOONGUSS, Species.CHARIZARD]);
const enemyPokemon = game.scene.getEnemyField();
const enemyStartingHp = enemyPokemon.map(p => p.hp);
game.move.select(Moves.SNIPE_SHOT, 0, BattlerIndex.ENEMY);
game.move.select(Moves.SNIPE_SHOT, 1, BattlerIndex.ENEMY_2);
await game.forceEnemyMove(Moves.FOLLOW_ME);
await game.forceEnemyMove(Moves.SPLASH);
await game.phaseInterceptor.to(TurnEndPhase, false);
// If redirection was bypassed, both enemies should be damaged
expect(enemyPokemon[0].hp).toBeLessThan(enemyStartingHp[0]);
expect(enemyPokemon[1].hp).toBeLessThan(enemyStartingHp[1]);
expect(enemyPokemon[0].hp).toBeLessThan(enemyPokemon[0].getMaxHp());
expect(enemyPokemon[1].hp).toBeLessThan(enemyPokemon[1].getMaxHp());
}, TIMEOUT
);
});

View File

@ -1,5 +1,4 @@
import { BattlerIndex } from "#app/battle";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
@ -31,27 +30,27 @@ describe("Moves - Rage Powder", () => {
game.override.startingLevel(100);
game.override.enemyLevel(100);
game.override.moveset([Moves.FOLLOW_ME, Moves.RAGE_POWDER, Moves.SPOTLIGHT, Moves.QUICK_ATTACK]);
game.override.enemyMoveset([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]);
game.override.enemyMoveset([Moves.RAGE_POWDER, Moves.TACKLE, Moves.SPLASH]);
});
test(
"move effect should be bypassed by Grass type",
async () => {
game.override.enemyMoveset([Moves.RAGE_POWDER, Moves.RAGE_POWDER, Moves.RAGE_POWDER, Moves.RAGE_POWDER]);
await game.startBattle([Species.AMOONGUSS, Species.VENUSAUR]);
await game.classicMode.startBattle([Species.AMOONGUSS, Species.VENUSAUR]);
const enemyPokemon = game.scene.getEnemyField();
const enemyStartingHp = enemyPokemon.map(p => p.hp);
game.move.select(Moves.QUICK_ATTACK, 0, BattlerIndex.ENEMY);
game.move.select(Moves.QUICK_ATTACK, 1, BattlerIndex.ENEMY_2);
await game.phaseInterceptor.to(TurnEndPhase, false);
await game.forceEnemyMove(Moves.RAGE_POWDER);
await game.forceEnemyMove(Moves.SPLASH);
await game.phaseInterceptor.to("BerryPhase", false);
// If redirection was bypassed, both enemies should be damaged
expect(enemyPokemon[0].hp).toBeLessThan(enemyStartingHp[0]);
expect(enemyPokemon[1].hp).toBeLessThan(enemyStartingHp[1]);
expect(enemyPokemon[0].hp).toBeLessThan(enemyPokemon[0].getMaxHp());
expect(enemyPokemon[1].hp).toBeLessThan(enemyPokemon[0].getMaxHp());
}, TIMEOUT
);
@ -59,10 +58,9 @@ describe("Moves - Rage Powder", () => {
"move effect should be bypassed by Overcoat",
async () => {
game.override.ability(Abilities.OVERCOAT);
game.override.enemyMoveset([Moves.RAGE_POWDER, Moves.RAGE_POWDER, Moves.RAGE_POWDER, Moves.RAGE_POWDER]);
// Test with two non-Grass type player Pokemon
await game.startBattle([Species.BLASTOISE, Species.CHARIZARD]);
await game.classicMode.startBattle([Species.BLASTOISE, Species.CHARIZARD]);
const enemyPokemon = game.scene.getEnemyField();
@ -70,7 +68,7 @@ describe("Moves - Rage Powder", () => {
game.move.select(Moves.QUICK_ATTACK, 0, BattlerIndex.ENEMY);
game.move.select(Moves.QUICK_ATTACK, 1, BattlerIndex.ENEMY_2);
await game.phaseInterceptor.to(TurnEndPhase, false);
await game.phaseInterceptor.to("BerryPhase", false);
// If redirection was bypassed, both enemies should be damaged
expect(enemyPokemon[0].hp).toBeLessThan(enemyStartingHp[0]);

View File

@ -73,7 +73,7 @@ describe("Moves - Spikes", () => {
await game.toNextTurn();
game.move.select(Moves.SPLASH);
game.forceOpponentToSwitch();
game.forceEnemyToSwitch();
await game.toNextTurn();
const enemy = game.scene.getEnemyParty()[0];

View File

@ -1,5 +1,4 @@
import { BattlerIndex } from "#app/battle";
import { Stat } from "#enums/stat";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
@ -31,52 +30,46 @@ describe("Moves - Spotlight", () => {
game.override.startingLevel(100);
game.override.enemyLevel(100);
game.override.moveset([Moves.FOLLOW_ME, Moves.RAGE_POWDER, Moves.SPOTLIGHT, Moves.QUICK_ATTACK]);
game.override.enemyMoveset([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]);
game.override.enemyMoveset([Moves.FOLLOW_ME, Moves.SPLASH]);
});
test(
"move should redirect attacks to the target",
async () => {
await game.startBattle([Species.AMOONGUSS, Species.CHARIZARD]);
await game.classicMode.startBattle([Species.AMOONGUSS, Species.CHARIZARD]);
const enemyPokemon = game.scene.getEnemyField();
const enemyStartingHp = enemyPokemon.map(p => p.hp);
game.move.select(Moves.SPOTLIGHT, 0, BattlerIndex.ENEMY);
game.move.select(Moves.QUICK_ATTACK, 1, BattlerIndex.ENEMY_2);
await game.forceEnemyMove(Moves.SPLASH);
await game.forceEnemyMove(Moves.SPLASH);
await game.phaseInterceptor.to(TurnEndPhase, false);
expect(enemyPokemon[0].hp).toBeLessThan(enemyStartingHp[0]);
expect(enemyPokemon[1].hp).toBe(enemyStartingHp[1]);
expect(enemyPokemon[0].hp).toBeLessThan(enemyPokemon[0].getMaxHp());
expect(enemyPokemon[1].hp).toBe(enemyPokemon[1].getMaxHp());
}, TIMEOUT
);
test(
"move should cause other redirection moves to fail",
async () => {
game.override.enemyMoveset([Moves.FOLLOW_ME, Moves.FOLLOW_ME, Moves.FOLLOW_ME, Moves.FOLLOW_ME]);
await game.startBattle([Species.AMOONGUSS, Species.CHARIZARD]);
await game.classicMode.startBattle([Species.AMOONGUSS, Species.CHARIZARD]);
const enemyPokemon = game.scene.getEnemyField();
/**
* Spotlight will target the slower enemy. In this situation without Spotlight being used,
* the faster enemy would normally end up with the Center of Attention tag.
*/
enemyPokemon.sort((a, b) => b.getEffectiveStat(Stat.SPD) - a.getEffectiveStat(Stat.SPD));
const spotTarget = enemyPokemon[1].getBattlerIndex();
const attackTarget = enemyPokemon[0].getBattlerIndex();
game.move.select(Moves.SPOTLIGHT, 0, BattlerIndex.ENEMY);
game.move.select(Moves.QUICK_ATTACK, 1, BattlerIndex.ENEMY_2);
const enemyStartingHp = enemyPokemon.map(p => p.hp);
await game.forceEnemyMove(Moves.SPLASH);
await game.forceEnemyMove(Moves.FOLLOW_ME);
game.move.select(Moves.SPOTLIGHT, 0, spotTarget);
game.move.select(Moves.QUICK_ATTACK, 1, attackTarget);
await game.phaseInterceptor.to(TurnEndPhase, false);
await game.phaseInterceptor.to("BerryPhase", false);
expect(enemyPokemon[1].hp).toBeLessThan(enemyStartingHp[1]);
expect(enemyPokemon[0].hp).toBe(enemyStartingHp[0]);
expect(enemyPokemon[0].hp).toBeLessThan(enemyPokemon[0].getMaxHp());
expect(enemyPokemon[1].hp).toBe(enemyPokemon[1].getMaxHp());
}, TIMEOUT
);
});

View File

@ -2,6 +2,8 @@ import { updateUserInfo } from "#app/account";
import { BattlerIndex } from "#app/battle";
import BattleScene from "#app/battle-scene";
import { BattleStyle } from "#app/enums/battle-style";
import { Moves } from "#app/enums/moves";
import { getMoveTargets } from "#app/data/move";
import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon";
import Trainer from "#app/field/trainer";
import { GameModes, getGameMode } from "#app/game-mode";
@ -9,6 +11,7 @@ import { ModifierTypeOption, modifierTypes } from "#app/modifier/modifier-type";
import overrides from "#app/overrides";
import { CommandPhase } from "#app/phases/command-phase";
import { EncounterPhase } from "#app/phases/encounter-phase";
import { EnemyCommandPhase } from "#app/phases/enemy-command-phase";
import { FaintPhase } from "#app/phases/faint-phase";
import { LoginPhase } from "#app/phases/login-phase";
import { MovePhase } from "#app/phases/move-phase";
@ -243,7 +246,34 @@ export default class GameManager {
}, () => this.isCurrentPhase(CommandPhase) || this.isCurrentPhase(NewBattlePhase) || this.isCurrentPhase(CheckSwitchPhase));
}
forceOpponentToSwitch() {
/**
* Forces the next enemy selecting a move to use the given move in its moveset against the
* given target (if applicable).
* @param moveId {@linkcode Moves} the move the enemy will use
* @param target {@linkcode BattlerIndex} the target on which the enemy will use the given move
*/
async forceEnemyMove(moveId: Moves, target?: BattlerIndex) {
// Wait for the next EnemyCommandPhase to start
await this.phaseInterceptor.to(EnemyCommandPhase, false);
const enemy = this.scene.getEnemyField()[(this.scene.getCurrentPhase() as EnemyCommandPhase).getFieldIndex()];
const legalTargets = getMoveTargets(enemy, moveId);
vi.spyOn(enemy, "getNextMove").mockReturnValueOnce({
move: moveId,
targets: (target && !legalTargets.multiple && legalTargets.targets.includes(target))
? [target]
: enemy.getNextTargets(moveId)
});
/**
* Run the EnemyCommandPhase to completion.
* This allows this function to be called consecutively to
* force a move for each enemy in a double battle.
*/
await this.phaseInterceptor.to(EnemyCommandPhase);
}
forceEnemyToSwitch() {
const originalMatchupScore = Trainer.prototype.getPartyMemberMatchupScores;
Trainer.prototype.getPartyMemberMatchupScores = () => {
Trainer.prototype.getPartyMemberMatchupScores = originalMatchupScore;