mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-01-18 23:11:11 +00:00
[Move] Implement Follow Me, Rage Powder, and Spotlight (#1834)
* Implement "Center of Attention" battler tag + moves * Powder immunity logic for Rage Powder * Center of Attention unit tests
This commit is contained in:
parent
d70dd16f8c
commit
3b3b9e39af
@ -1009,6 +1009,33 @@ export class PerishSongTag extends BattlerTag {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies the "Center of Attention" volatile status effect, the effect applied by Follow Me, Rage Powder, and Spotlight.
|
||||||
|
* @see {@link https://bulbapedia.bulbagarden.net/wiki/Center_of_attention | Center of Attention}
|
||||||
|
*/
|
||||||
|
export class CenterOfAttentionTag extends BattlerTag {
|
||||||
|
public powder: boolean;
|
||||||
|
|
||||||
|
constructor(sourceMove: Moves) {
|
||||||
|
super(BattlerTagType.CENTER_OF_ATTENTION, BattlerTagLapseType.TURN_END, 1, sourceMove);
|
||||||
|
|
||||||
|
this.powder = (this.sourceMove === Moves.RAGE_POWDER);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** "Center of Attention" can't be added if an ally is already the Center of Attention. */
|
||||||
|
canAdd(pokemon: Pokemon): boolean {
|
||||||
|
const activeTeam = pokemon.isPlayer() ? pokemon.scene.getPlayerField() : pokemon.scene.getEnemyField();
|
||||||
|
|
||||||
|
return !activeTeam.find(p => p.getTag(BattlerTagType.CENTER_OF_ATTENTION));
|
||||||
|
}
|
||||||
|
|
||||||
|
onAdd(pokemon: Pokemon): void {
|
||||||
|
super.onAdd(pokemon);
|
||||||
|
|
||||||
|
pokemon.scene.queueMessage(getPokemonMessage(pokemon, " became the center\nof attention!"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class AbilityBattlerTag extends BattlerTag {
|
export class AbilityBattlerTag extends BattlerTag {
|
||||||
public ability: Abilities;
|
public ability: Abilities;
|
||||||
|
|
||||||
@ -1494,6 +1521,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: integer, sourc
|
|||||||
return new SturdyTag(sourceMove);
|
return new SturdyTag(sourceMove);
|
||||||
case BattlerTagType.PERISH_SONG:
|
case BattlerTagType.PERISH_SONG:
|
||||||
return new PerishSongTag(turnCount);
|
return new PerishSongTag(turnCount);
|
||||||
|
case BattlerTagType.CENTER_OF_ATTENTION:
|
||||||
|
return new CenterOfAttentionTag(sourceMove);
|
||||||
case BattlerTagType.TRUANT:
|
case BattlerTagType.TRUANT:
|
||||||
return new TruantTag();
|
return new TruantTag();
|
||||||
case BattlerTagType.SLOW_START:
|
case BattlerTagType.SLOW_START:
|
||||||
|
@ -284,6 +284,10 @@ export default class Move implements Localizable {
|
|||||||
* @returns boolean
|
* @returns boolean
|
||||||
*/
|
*/
|
||||||
isTypeImmune(type: Type): boolean {
|
isTypeImmune(type: Type): boolean {
|
||||||
|
if (this.moveTarget === MoveTarget.USER) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case Type.GRASS:
|
case Type.GRASS:
|
||||||
if (this.hasFlag(MoveFlags.POWDER_MOVE)) {
|
if (this.hasFlag(MoveFlags.POWDER_MOVE)) {
|
||||||
@ -6301,7 +6305,7 @@ export function initMoves() {
|
|||||||
.attr(MovePowerMultiplierAttr, (user, target, move) => target.status?.effect === StatusEffect.PARALYSIS ? 2 : 1)
|
.attr(MovePowerMultiplierAttr, (user, target, move) => target.status?.effect === StatusEffect.PARALYSIS ? 2 : 1)
|
||||||
.attr(HealStatusEffectAttr, true, StatusEffect.PARALYSIS),
|
.attr(HealStatusEffectAttr, true, StatusEffect.PARALYSIS),
|
||||||
new SelfStatusMove(Moves.FOLLOW_ME, Type.NORMAL, -1, 20, -1, 2, 3)
|
new SelfStatusMove(Moves.FOLLOW_ME, Type.NORMAL, -1, 20, -1, 2, 3)
|
||||||
.unimplemented(),
|
.attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, true),
|
||||||
new StatusMove(Moves.NATURE_POWER, Type.NORMAL, -1, 20, -1, 0, 3)
|
new StatusMove(Moves.NATURE_POWER, Type.NORMAL, -1, 20, -1, 0, 3)
|
||||||
.attr(NaturePowerAttr)
|
.attr(NaturePowerAttr)
|
||||||
.ignoresVirtual(),
|
.ignoresVirtual(),
|
||||||
@ -6866,7 +6870,7 @@ export function initMoves() {
|
|||||||
.partial(),
|
.partial(),
|
||||||
new SelfStatusMove(Moves.RAGE_POWDER, Type.BUG, -1, 20, -1, 2, 5)
|
new SelfStatusMove(Moves.RAGE_POWDER, Type.BUG, -1, 20, -1, 2, 5)
|
||||||
.powderMove()
|
.powderMove()
|
||||||
.unimplemented(),
|
.attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, true),
|
||||||
new StatusMove(Moves.TELEKINESIS, Type.PSYCHIC, -1, 15, -1, 0, 5)
|
new StatusMove(Moves.TELEKINESIS, Type.PSYCHIC, -1, 15, -1, 0, 5)
|
||||||
.condition(failOnGravityCondition)
|
.condition(failOnGravityCondition)
|
||||||
.unimplemented(),
|
.unimplemented(),
|
||||||
@ -7434,7 +7438,7 @@ export function initMoves() {
|
|||||||
new AttackMove(Moves.LEAFAGE, Type.GRASS, MoveCategory.PHYSICAL, 40, 100, 40, -1, 0, 7)
|
new AttackMove(Moves.LEAFAGE, Type.GRASS, MoveCategory.PHYSICAL, 40, 100, 40, -1, 0, 7)
|
||||||
.makesContact(false),
|
.makesContact(false),
|
||||||
new StatusMove(Moves.SPOTLIGHT, Type.NORMAL, -1, 15, -1, 3, 7)
|
new StatusMove(Moves.SPOTLIGHT, Type.NORMAL, -1, 15, -1, 3, 7)
|
||||||
.unimplemented(),
|
.attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, false),
|
||||||
new StatusMove(Moves.TOXIC_THREAD, Type.POISON, 100, 20, 100, 0, 7)
|
new StatusMove(Moves.TOXIC_THREAD, Type.POISON, 100, 20, 100, 0, 7)
|
||||||
.attr(StatusEffectAttr, StatusEffect.POISON)
|
.attr(StatusEffectAttr, StatusEffect.POISON)
|
||||||
.attr(StatChangeAttr, BattleStat.SPD, -1),
|
.attr(StatChangeAttr, BattleStat.SPD, -1),
|
||||||
|
@ -58,5 +58,6 @@ export enum BattlerTagType {
|
|||||||
MAGNET_RISEN = "MAGNET_RISEN",
|
MAGNET_RISEN = "MAGNET_RISEN",
|
||||||
MINIMIZED = "MINIMIZED",
|
MINIMIZED = "MINIMIZED",
|
||||||
DESTINY_BOND = "DESTINY_BOND",
|
DESTINY_BOND = "DESTINY_BOND",
|
||||||
|
CENTER_OF_ATTENTION = "CENTER_OF_ATTENTION",
|
||||||
ICE_FACE = "ICE_FACE"
|
ICE_FACE = "ICE_FACE"
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ import { biomeLinks, getBiomeName } from "./data/biomes";
|
|||||||
import { ModifierTier } from "./modifier/modifier-tier";
|
import { ModifierTier } from "./modifier/modifier-tier";
|
||||||
import { FusePokemonModifierType, ModifierPoolType, ModifierType, ModifierTypeFunc, ModifierTypeOption, PokemonModifierType, PokemonMoveModifierType, PokemonPpRestoreModifierType, PokemonPpUpModifierType, RememberMoveModifierType, TmModifierType, getDailyRunStarterModifiers, getEnemyBuffModifierForWave, getModifierType, getPlayerModifierTypeOptions, getPlayerShopModifierTypeOptionsForWave, modifierTypes, regenerateModifierPoolThresholds } from "./modifier/modifier-type";
|
import { FusePokemonModifierType, ModifierPoolType, ModifierType, ModifierTypeFunc, ModifierTypeOption, PokemonModifierType, PokemonMoveModifierType, PokemonPpRestoreModifierType, PokemonPpUpModifierType, RememberMoveModifierType, TmModifierType, getDailyRunStarterModifiers, getEnemyBuffModifierForWave, getModifierType, getPlayerModifierTypeOptions, getPlayerShopModifierTypeOptionsForWave, modifierTypes, regenerateModifierPoolThresholds } from "./modifier/modifier-type";
|
||||||
import SoundFade from "phaser3-rex-plugins/plugins/soundfade";
|
import SoundFade from "phaser3-rex-plugins/plugins/soundfade";
|
||||||
import { BattlerTagLapseType, EncoreTag, HideSpriteTag as HiddenTag, ProtectedTag, TrappedTag } from "./data/battler-tags";
|
import { BattlerTagLapseType, CenterOfAttentionTag, EncoreTag, HideSpriteTag as HiddenTag, ProtectedTag, TrappedTag } from "./data/battler-tags";
|
||||||
import { getPokemonMessage, getPokemonNameWithAffix } from "./messages";
|
import { getPokemonMessage, getPokemonNameWithAffix } from "./messages";
|
||||||
import { Starter } from "./ui/starter-select-ui-handler";
|
import { Starter } from "./ui/starter-select-ui-handler";
|
||||||
import { Gender } from "./data/gender";
|
import { Gender } from "./data/gender";
|
||||||
@ -2606,6 +2606,12 @@ export class MovePhase extends BattlePhase {
|
|||||||
if (moveTarget) {
|
if (moveTarget) {
|
||||||
const oldTarget = moveTarget.value;
|
const oldTarget = moveTarget.value;
|
||||||
this.scene.getField(true).filter(p => p !== this.pokemon).forEach(p => applyAbAttrs(RedirectMoveAbAttr, p, null, this.move.moveId, moveTarget));
|
this.scene.getField(true).filter(p => p !== this.pokemon).forEach(p => applyAbAttrs(RedirectMoveAbAttr, p, null, this.move.moveId, moveTarget));
|
||||||
|
this.pokemon.getOpponents().forEach(p => {
|
||||||
|
const redirectTag = p.getTag(CenterOfAttentionTag) as CenterOfAttentionTag;
|
||||||
|
if (redirectTag && (!redirectTag.powder || (!this.pokemon.isOfType(Type.GRASS) && !this.pokemon.hasAbility(Abilities.OVERCOAT)))) {
|
||||||
|
moveTarget.value = p.getBattlerIndex();
|
||||||
|
}
|
||||||
|
});
|
||||||
//Check if this move is immune to being redirected, and restore its target to the intended target if it is.
|
//Check if this move is immune to being redirected, and restore its target to the intended target if it is.
|
||||||
if ((this.pokemon.hasAbilityWithAttr(BlockRedirectAbAttr) || this.move.getMove().hasAttr(BypassRedirectAttr))) {
|
if ((this.pokemon.hasAbilityWithAttr(BlockRedirectAbAttr) || this.move.getMove().hasAttr(BypassRedirectAttr))) {
|
||||||
//If an ability prevented this move from being redirected, display its ability pop up.
|
//If an ability prevented this move from being redirected, display its ability pop up.
|
||||||
|
166
src/test/moves/follow_me.test.ts
Normal file
166
src/test/moves/follow_me.test.ts
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
import {afterEach, beforeAll, beforeEach, describe, expect, test, vi} from "vitest";
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import GameManager from "#app/test/utils/gameManager";
|
||||||
|
import * as overrides from "#app/overrides";
|
||||||
|
import {
|
||||||
|
CommandPhase,
|
||||||
|
SelectTargetPhase,
|
||||||
|
TurnEndPhase,
|
||||||
|
} from "#app/phases";
|
||||||
|
import {Stat} from "#app/data/pokemon-stat";
|
||||||
|
import {getMovePosition} from "#app/test/utils/gameManagerUtils";
|
||||||
|
import { Moves } from "#enums/moves";
|
||||||
|
import { Species } from "#enums/species";
|
||||||
|
import { BattlerIndex } from "#app/battle.js";
|
||||||
|
import { Abilities } from "#app/enums/abilities.js";
|
||||||
|
|
||||||
|
const TIMEOUT = 20 * 1000;
|
||||||
|
|
||||||
|
describe("Moves - Follow Me", () => {
|
||||||
|
let phaserGame: Phaser.Game;
|
||||||
|
let game: GameManager;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
|
||||||
|
vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.AMOONGUSS);
|
||||||
|
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.SNORLAX);
|
||||||
|
vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(100);
|
||||||
|
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([ Moves.FOLLOW_ME, Moves.RAGE_POWDER, Moves.SPOTLIGHT, Moves.QUICK_ATTACK ]);
|
||||||
|
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE,Moves.TACKLE,Moves.TACKLE,Moves.TACKLE]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
"move should redirect enemy attacks to the user",
|
||||||
|
async () => {
|
||||||
|
await game.startBattle([ Species.AMOONGUSS, Species.CHARIZARD ]);
|
||||||
|
|
||||||
|
const playerPokemon = game.scene.getPlayerField();
|
||||||
|
expect(playerPokemon.length).toBe(2);
|
||||||
|
playerPokemon.forEach(p => expect(p).not.toBe(undefined));
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyField();
|
||||||
|
expect(enemyPokemon.length).toBe(2);
|
||||||
|
enemyPokemon.forEach(p => expect(p).not.toBe(undefined));
|
||||||
|
|
||||||
|
playerPokemon.forEach(p => p.hp = 200);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.FOLLOW_ME));
|
||||||
|
await game.phaseInterceptor.to(CommandPhase);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 1, Moves.QUICK_ATTACK));
|
||||||
|
await game.phaseInterceptor.to(SelectTargetPhase, false);
|
||||||
|
|
||||||
|
game.doSelectTarget(BattlerIndex.ENEMY);
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
expect(playerPokemon[0].hp).toBeLessThan(200);
|
||||||
|
expect(playerPokemon[1].hp).toBe(200);
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"move should redirect enemy attacks to the first ally that uses it",
|
||||||
|
async () => {
|
||||||
|
await game.startBattle([ Species.AMOONGUSS, Species.CHARIZARD ]);
|
||||||
|
|
||||||
|
const playerPokemon = game.scene.getPlayerField();
|
||||||
|
expect(playerPokemon.length).toBe(2);
|
||||||
|
playerPokemon.forEach(p => expect(p).not.toBe(undefined));
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyField();
|
||||||
|
expect(enemyPokemon.length).toBe(2);
|
||||||
|
enemyPokemon.forEach(p => expect(p).not.toBe(undefined));
|
||||||
|
|
||||||
|
playerPokemon.forEach(p => p.hp = 200);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.FOLLOW_ME));
|
||||||
|
await game.phaseInterceptor.to(CommandPhase);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 1, Moves.FOLLOW_ME));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
playerPokemon.sort((a, b) => a.getBattleStat(Stat.SPD) - b.getBattleStat(Stat.SPD));
|
||||||
|
|
||||||
|
expect(playerPokemon[1].hp).toBeLessThan(200);
|
||||||
|
expect(playerPokemon[0].hp).toBe(200);
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"move effect should be bypassed by Stalwart",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.STALWART);
|
||||||
|
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([ Moves.QUICK_ATTACK ]);
|
||||||
|
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([ Moves.FOLLOW_ME, Moves.FOLLOW_ME, Moves.FOLLOW_ME, Moves.FOLLOW_ME ]);
|
||||||
|
|
||||||
|
await game.startBattle([ Species.AMOONGUSS, Species.CHARIZARD ]);
|
||||||
|
|
||||||
|
const playerPokemon = game.scene.getPlayerField();
|
||||||
|
expect(playerPokemon.length).toBe(2);
|
||||||
|
playerPokemon.forEach(p => expect(p).not.toBe(undefined));
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyField();
|
||||||
|
expect(enemyPokemon.length).toBe(2);
|
||||||
|
enemyPokemon.forEach(p => expect(p).not.toBe(undefined));
|
||||||
|
|
||||||
|
enemyPokemon.forEach(p => p.hp = 200);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.QUICK_ATTACK));
|
||||||
|
await game.phaseInterceptor.to(SelectTargetPhase, false);
|
||||||
|
game.doSelectTarget(BattlerIndex.ENEMY);
|
||||||
|
await game.phaseInterceptor.to(CommandPhase);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 1, Moves.QUICK_ATTACK));
|
||||||
|
await game.phaseInterceptor.to(SelectTargetPhase, false);
|
||||||
|
game.doSelectTarget(BattlerIndex.ENEMY_2);
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
// If redirection was bypassed, both enemies should be damaged
|
||||||
|
enemyPokemon.forEach(p => expect(p.hp).toBeLessThan(200));
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"move effect should be bypassed by Snipe Shot",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([ Moves.SNIPE_SHOT ]);
|
||||||
|
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([ Moves.FOLLOW_ME, Moves.FOLLOW_ME, Moves.FOLLOW_ME, Moves.FOLLOW_ME ]);
|
||||||
|
|
||||||
|
await game.startBattle([ Species.AMOONGUSS, Species.CHARIZARD ]);
|
||||||
|
|
||||||
|
const playerPokemon = game.scene.getPlayerField();
|
||||||
|
expect(playerPokemon.length).toBe(2);
|
||||||
|
playerPokemon.forEach(p => expect(p).not.toBe(undefined));
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyField();
|
||||||
|
expect(enemyPokemon.length).toBe(2);
|
||||||
|
enemyPokemon.forEach(p => expect(p).not.toBe(undefined));
|
||||||
|
|
||||||
|
enemyPokemon.forEach(p => p.hp = 200);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.SNIPE_SHOT));
|
||||||
|
await game.phaseInterceptor.to(SelectTargetPhase, false);
|
||||||
|
game.doSelectTarget(BattlerIndex.ENEMY);
|
||||||
|
await game.phaseInterceptor.to(CommandPhase);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 1, Moves.SNIPE_SHOT));
|
||||||
|
await game.phaseInterceptor.to(SelectTargetPhase, false);
|
||||||
|
game.doSelectTarget(BattlerIndex.ENEMY_2);
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
// If redirection was bypassed, both enemies should be damaged
|
||||||
|
enemyPokemon.forEach(p => expect(p.hp).toBeLessThan(200));
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
});
|
107
src/test/moves/rage_powder.test.ts
Normal file
107
src/test/moves/rage_powder.test.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import {afterEach, beforeAll, beforeEach, describe, expect, test, vi} from "vitest";
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import GameManager from "#app/test/utils/gameManager";
|
||||||
|
import * as overrides from "#app/overrides";
|
||||||
|
import {
|
||||||
|
CommandPhase,
|
||||||
|
SelectTargetPhase,
|
||||||
|
TurnEndPhase,
|
||||||
|
} from "#app/phases";
|
||||||
|
import {getMovePosition} from "#app/test/utils/gameManagerUtils";
|
||||||
|
import { Abilities } from "#enums/abilities";
|
||||||
|
import { Moves } from "#enums/moves";
|
||||||
|
import { Species } from "#enums/species";
|
||||||
|
import { BattlerIndex } from "#app/battle.js";
|
||||||
|
|
||||||
|
const TIMEOUT = 20 * 1000;
|
||||||
|
|
||||||
|
describe("Moves - Rage Powder", () => {
|
||||||
|
let phaserGame: Phaser.Game;
|
||||||
|
let game: GameManager;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
|
||||||
|
vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.AMOONGUSS);
|
||||||
|
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.SNORLAX);
|
||||||
|
vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(100);
|
||||||
|
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([ Moves.FOLLOW_ME, Moves.RAGE_POWDER, Moves.SPOTLIGHT, Moves.QUICK_ATTACK ]);
|
||||||
|
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE,Moves.TACKLE,Moves.TACKLE,Moves.TACKLE]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
"move effect should be bypassed by Grass type",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([ Moves.RAGE_POWDER, Moves.RAGE_POWDER, Moves.RAGE_POWDER, Moves.RAGE_POWDER ]);
|
||||||
|
|
||||||
|
await game.startBattle([ Species.AMOONGUSS, Species.VENUSAUR ]);
|
||||||
|
|
||||||
|
const playerPokemon = game.scene.getPlayerField();
|
||||||
|
expect(playerPokemon.length).toBe(2);
|
||||||
|
playerPokemon.forEach(p => expect(p).not.toBe(undefined));
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyField();
|
||||||
|
expect(enemyPokemon.length).toBe(2);
|
||||||
|
enemyPokemon.forEach(p => expect(p).not.toBe(undefined));
|
||||||
|
|
||||||
|
enemyPokemon.forEach(p => p.hp = 200);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.QUICK_ATTACK));
|
||||||
|
await game.phaseInterceptor.to(SelectTargetPhase, false);
|
||||||
|
game.doSelectTarget(BattlerIndex.ENEMY);
|
||||||
|
await game.phaseInterceptor.to(CommandPhase);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 1, Moves.QUICK_ATTACK));
|
||||||
|
await game.phaseInterceptor.to(SelectTargetPhase, false);
|
||||||
|
game.doSelectTarget(BattlerIndex.ENEMY_2);
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
// If redirection was bypassed, both enemies should be damaged
|
||||||
|
enemyPokemon.forEach(p => expect(p.hp).toBeLessThan(200));
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"move effect should be bypassed by Overcoat",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.OVERCOAT);
|
||||||
|
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([ 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 ]);
|
||||||
|
|
||||||
|
const playerPokemon = game.scene.getPlayerField();
|
||||||
|
expect(playerPokemon.length).toBe(2);
|
||||||
|
playerPokemon.forEach(p => expect(p).not.toBe(undefined));
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyField();
|
||||||
|
expect(enemyPokemon.length).toBe(2);
|
||||||
|
enemyPokemon.forEach(p => expect(p).not.toBe(undefined));
|
||||||
|
|
||||||
|
enemyPokemon.forEach(p => p.hp = 200);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.QUICK_ATTACK));
|
||||||
|
await game.phaseInterceptor.to(SelectTargetPhase, false);
|
||||||
|
game.doSelectTarget(BattlerIndex.ENEMY);
|
||||||
|
await game.phaseInterceptor.to(CommandPhase);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 1, Moves.QUICK_ATTACK));
|
||||||
|
await game.phaseInterceptor.to(SelectTargetPhase, false);
|
||||||
|
game.doSelectTarget(BattlerIndex.ENEMY_2);
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
// If redirection was bypassed, both enemies should be damaged
|
||||||
|
enemyPokemon.forEach(p => expect(p.hp).toBeLessThan(200));
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
});
|
111
src/test/moves/spotlight.test.ts
Normal file
111
src/test/moves/spotlight.test.ts
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import {afterEach, beforeAll, beforeEach, describe, expect, test, vi} from "vitest";
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import GameManager from "#app/test/utils/gameManager";
|
||||||
|
import * as overrides from "#app/overrides";
|
||||||
|
import {
|
||||||
|
CommandPhase,
|
||||||
|
SelectTargetPhase,
|
||||||
|
TurnEndPhase,
|
||||||
|
} from "#app/phases";
|
||||||
|
import {Stat} from "#app/data/pokemon-stat";
|
||||||
|
import {getMovePosition} from "#app/test/utils/gameManagerUtils";
|
||||||
|
import { Moves } from "#enums/moves";
|
||||||
|
import { Species } from "#enums/species";
|
||||||
|
import { BattlerIndex } from "#app/battle.js";
|
||||||
|
|
||||||
|
const TIMEOUT = 20 * 1000;
|
||||||
|
|
||||||
|
describe("Moves - Spotlight", () => {
|
||||||
|
let phaserGame: Phaser.Game;
|
||||||
|
let game: GameManager;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
|
||||||
|
vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.AMOONGUSS);
|
||||||
|
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.SNORLAX);
|
||||||
|
vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(100);
|
||||||
|
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([ Moves.FOLLOW_ME, Moves.RAGE_POWDER, Moves.SPOTLIGHT, Moves.QUICK_ATTACK ]);
|
||||||
|
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE,Moves.TACKLE,Moves.TACKLE,Moves.TACKLE]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
"move should redirect attacks to the target",
|
||||||
|
async () => {
|
||||||
|
await game.startBattle([ Species.AMOONGUSS, Species.CHARIZARD ]);
|
||||||
|
|
||||||
|
const playerPokemon = game.scene.getPlayerField();
|
||||||
|
expect(playerPokemon.length).toBe(2);
|
||||||
|
playerPokemon.forEach(p => expect(p).not.toBe(undefined));
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyField();
|
||||||
|
expect(enemyPokemon.length).toBe(2);
|
||||||
|
enemyPokemon.forEach(p => expect(p).not.toBe(undefined));
|
||||||
|
|
||||||
|
enemyPokemon.forEach(p => p.hp = 200);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.SPOTLIGHT));
|
||||||
|
await game.phaseInterceptor.to(SelectTargetPhase, false);
|
||||||
|
game.doSelectTarget(BattlerIndex.ENEMY);
|
||||||
|
await game.phaseInterceptor.to(CommandPhase);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 1, Moves.QUICK_ATTACK));
|
||||||
|
await game.phaseInterceptor.to(SelectTargetPhase, false);
|
||||||
|
game.doSelectTarget(BattlerIndex.ENEMY_2);
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
expect(enemyPokemon[0].hp).toBeLessThan(200);
|
||||||
|
expect(enemyPokemon[1].hp).toBe(200);
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"move should cause other redirection moves to fail",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([ Moves.FOLLOW_ME, Moves.FOLLOW_ME, Moves.FOLLOW_ME, Moves.FOLLOW_ME ]);
|
||||||
|
|
||||||
|
await game.startBattle([ Species.AMOONGUSS, Species.CHARIZARD ]);
|
||||||
|
|
||||||
|
const playerPokemon = game.scene.getPlayerField();
|
||||||
|
expect(playerPokemon.length).toBe(2);
|
||||||
|
playerPokemon.forEach(p => expect(p).not.toBe(undefined));
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyField();
|
||||||
|
expect(enemyPokemon.length).toBe(2);
|
||||||
|
enemyPokemon.forEach(p => expect(p).not.toBe(undefined));
|
||||||
|
|
||||||
|
enemyPokemon.forEach(p => p.hp = 200);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.getBattleStat(Stat.SPD) - a.getBattleStat(Stat.SPD));
|
||||||
|
const spotTarget = enemyPokemon[1].getBattlerIndex();
|
||||||
|
const attackTarget = enemyPokemon[0].getBattlerIndex();
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.SPOTLIGHT));
|
||||||
|
await game.phaseInterceptor.to(SelectTargetPhase, false);
|
||||||
|
game.doSelectTarget(spotTarget);
|
||||||
|
await game.phaseInterceptor.to(CommandPhase);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 1, Moves.QUICK_ATTACK));
|
||||||
|
await game.phaseInterceptor.to(SelectTargetPhase, false);
|
||||||
|
game.doSelectTarget(attackTarget);
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
expect(enemyPokemon[1].hp).toBeLessThan(200);
|
||||||
|
expect(enemyPokemon[0].hp).toBe(200);
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
});
|
@ -5,9 +5,11 @@ import {
|
|||||||
CommandPhase,
|
CommandPhase,
|
||||||
EncounterPhase,
|
EncounterPhase,
|
||||||
FaintPhase,
|
FaintPhase,
|
||||||
LoginPhase, NewBattlePhase,
|
LoginPhase,
|
||||||
|
NewBattlePhase,
|
||||||
SelectStarterPhase,
|
SelectStarterPhase,
|
||||||
TitlePhase, TurnInitPhase,
|
TitlePhase, TurnInitPhase,
|
||||||
|
TurnStartPhase,
|
||||||
} from "#app/phases";
|
} from "#app/phases";
|
||||||
import BattleScene from "#app/battle-scene.js";
|
import BattleScene from "#app/battle-scene.js";
|
||||||
import PhaseInterceptor from "#app/test/utils/phaseInterceptor";
|
import PhaseInterceptor from "#app/test/utils/phaseInterceptor";
|
||||||
@ -29,6 +31,8 @@ import { GameDataType } from "#enums/game-data-type";
|
|||||||
import { PlayerGender } from "#enums/player-gender";
|
import { PlayerGender } from "#enums/player-gender";
|
||||||
import { Species } from "#enums/species";
|
import { Species } from "#enums/species";
|
||||||
import { Button } from "#enums/buttons";
|
import { Button } from "#enums/buttons";
|
||||||
|
import { BattlerIndex } from "#app/battle.js";
|
||||||
|
import TargetSelectUiHandler from "#app/ui/target-select-ui-handler.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to manage the game state and transitions between phases.
|
* Class to manage the game state and transitions between phases.
|
||||||
@ -168,6 +172,19 @@ export default class GameManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emulate a player's target selection after an attack is chosen,
|
||||||
|
* usually called after {@linkcode doAttack} in a double battle.
|
||||||
|
* @param {BattlerIndex} targetIndex the index of the attack target
|
||||||
|
*/
|
||||||
|
doSelectTarget(targetIndex: BattlerIndex) {
|
||||||
|
this.onNextPrompt("SelectTargetPhase", Mode.TARGET_SELECT, () => {
|
||||||
|
const handler = this.scene.ui.getHandler() as TargetSelectUiHandler;
|
||||||
|
handler.setCursor(targetIndex);
|
||||||
|
handler.processInput(Button.ACTION);
|
||||||
|
}, () => this.isCurrentPhase(CommandPhase) || this.isCurrentPhase(TurnStartPhase));
|
||||||
|
}
|
||||||
|
|
||||||
/** Faint all opponents currently on the field */
|
/** Faint all opponents currently on the field */
|
||||||
async doKillOpponents() {
|
async doKillOpponents() {
|
||||||
await this.killPokemon(this.scene.currentBattle.enemyParty[0]);
|
await this.killPokemon(this.scene.currentBattle.enemyParty[0]);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user