mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-06-20 16:42:45 +02:00
Compare commits
19 Commits
c754c3905b
...
f0c58fa687
Author | SHA1 | Date | |
---|---|---|---|
|
f0c58fa687 | ||
|
1ff2701964 | ||
|
1e306e25b5 | ||
|
43aa772603 | ||
|
b0fa48f8c8 | ||
|
e4115a362b | ||
|
06b6143931 | ||
|
e59bf5254f | ||
|
4e9655e129 | ||
|
9c2119eaed | ||
|
a7d699bf97 | ||
|
cc44067c89 | ||
|
a1b00c8b56 | ||
|
cd6f8b9c3d | ||
|
a66263fd6a | ||
|
adea255d35 | ||
|
fd290386b3 | ||
|
3e83b3d9d4 | ||
|
e7bb0d2e1e |
@ -1089,12 +1089,10 @@ export class GravityTag extends ArenaTag {
|
||||
onAdd(_arena: Arena): void {
|
||||
globalScene.phaseManager.queueMessage(i18next.t("arenaTag:gravityOnAdd"));
|
||||
globalScene.getField(true).forEach(pokemon => {
|
||||
if (pokemon !== null) {
|
||||
pokemon.removeTag(BattlerTagType.FLOATING);
|
||||
pokemon.removeTag(BattlerTagType.TELEKINESIS);
|
||||
if (pokemon.getTag(BattlerTagType.FLYING)) {
|
||||
pokemon.addTag(BattlerTagType.INTERRUPTED);
|
||||
}
|
||||
pokemon.removeTag(BattlerTagType.FLOATING);
|
||||
pokemon.removeTag(BattlerTagType.TELEKINESIS);
|
||||
if (pokemon.getTag(BattlerTagType.FLYING)) {
|
||||
pokemon.addTag(BattlerTagType.INTERRUPTED);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -633,6 +633,10 @@ export class FlinchedTag extends BattlerTag {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag to cancel the target's action when knocked out of a flying move by Smack Down or Gravity.
|
||||
*/
|
||||
// TODO: This is not a very good way to cancel a semi invulnerable turn
|
||||
export class InterruptedTag extends BattlerTag {
|
||||
constructor(sourceMove: MoveId) {
|
||||
super(BattlerTagType.INTERRUPTED, BattlerTagLapseType.PRE_MOVE, 0, sourceMove);
|
||||
|
@ -93,6 +93,10 @@ import { ChargingMove, MoveAttrMap, MoveAttrString, MoveKindString, MoveClassMap
|
||||
import { applyMoveAttrs } from "./apply-attrs";
|
||||
import { frenzyMissFunc, getMoveTargets } from "./move-utils";
|
||||
|
||||
/**
|
||||
* A function used to conditionally determine execution of a given {@linkcode MoveAttr}.
|
||||
* Conventionally returns `true` for success and `false` for failure.
|
||||
*/
|
||||
type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean;
|
||||
export type UserMoveConditionFunc = (user: Pokemon, move: Move) => boolean;
|
||||
|
||||
@ -1390,18 +1394,31 @@ export class BeakBlastHeaderAttr extends AddBattlerTagHeaderAttr {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute to display a message before a move is executed.
|
||||
*/
|
||||
export class PreMoveMessageAttr extends MoveAttr {
|
||||
private message: string | ((user: Pokemon, target: Pokemon, move: Move) => string);
|
||||
/** The message to display or a function returning one */
|
||||
private message: string | ((user: Pokemon, target: Pokemon, move: Move) => string | undefined);
|
||||
|
||||
/**
|
||||
* Create a new {@linkcode PreMoveMessageAttr} to display a message before move execution.
|
||||
* @param message - The message to display before move use, either as a string or a function producing one.
|
||||
* @remarks
|
||||
* If {@linkcode message} evaluates to an empty string (`''`), no message will be displayed
|
||||
* (though the move will still succeed).
|
||||
*/
|
||||
constructor(message: string | ((user: Pokemon, target: Pokemon, move: Move) => string)) {
|
||||
super();
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
const message = typeof this.message === "string"
|
||||
? this.message as string
|
||||
: this.message(user, target, move);
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, _args: any[]): boolean {
|
||||
const message = typeof this.message === "function"
|
||||
? this.message(user, target, move)
|
||||
: this.message;
|
||||
|
||||
// TODO: Consider changing if/when MoveAttr `apply` return values become significant
|
||||
if (message) {
|
||||
globalScene.phaseManager.queueMessage(message, 500);
|
||||
return true;
|
||||
@ -5278,13 +5295,11 @@ export class VariableMoveTypeMultiplierAttr extends MoveAttr {
|
||||
}
|
||||
|
||||
export class NeutralDamageAgainstFlyingTypeMultiplierAttr extends VariableMoveTypeMultiplierAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (!target.getTag(BattlerTagType.IGNORE_FLYING)) {
|
||||
const multiplier = args[0] as NumberHolder;
|
||||
//When a flying type is hit, the first hit is always 1x multiplier.
|
||||
if (target.isOfType(PokemonType.FLYING)) {
|
||||
multiplier.value = 1;
|
||||
}
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: [NumberHolder]): boolean {
|
||||
if (!target.isGrounded(true) && target.isOfType(PokemonType.FLYING)) {
|
||||
const multiplier = args[0];
|
||||
// When a flying type is hit, the first hit is always 1x multiplier.
|
||||
multiplier.value = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -5620,8 +5635,8 @@ export class LeechSeedAttr extends AddBattlerTagAttr {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the appropriate battler tag for Smack Down and Thousand arrows
|
||||
* @extends AddBattlerTagAttr
|
||||
* Attribute to add the {@linkcode BattlerTagType.IGNORE_FLYING | IGNORE_FLYING} battler tag to the target
|
||||
* and remove any prior sources of ungroundedness.
|
||||
*/
|
||||
export class FallDownAttr extends AddBattlerTagAttr {
|
||||
constructor() {
|
||||
@ -5630,16 +5645,28 @@ export class FallDownAttr extends AddBattlerTagAttr {
|
||||
|
||||
/**
|
||||
* Adds Grounded Tag to the target and checks if fallDown message should be displayed
|
||||
* @param user the {@linkcode Pokemon} using the move
|
||||
* @param target the {@linkcode Pokemon} targeted by the move
|
||||
* @param move the {@linkcode Move} invoking this effect
|
||||
* @param user - The {@linkcode Pokemon} using the move
|
||||
* @param target - The {@linkcode Pokemon} targeted by the move
|
||||
* @param move - The {@linkcode Move} invoking this effect
|
||||
* @param args n/a
|
||||
* @returns `true` if the effect successfully applies; `false` otherwise
|
||||
* @returns Whether the target was successfully brought down to earth.
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (!target.isGrounded()) {
|
||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:fallDown", { targetPokemonName: getPokemonNameWithAffix(target) }));
|
||||
// Smack down and similar only add their tag if the pokemon is already ungrounded
|
||||
// TODO: Does this work if the target is semi-invulnerable?
|
||||
if (target.isGrounded(true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove the target's flying/floating state and telekinesis, interrupting fly/bounce's activation.
|
||||
// TODO: This is not a good way to remove flying...
|
||||
target.removeTag(BattlerTagType.FLOATING);
|
||||
target.removeTag(BattlerTagType.TELEKINESIS);
|
||||
if (target.getTag(BattlerTagType.FLYING)) {
|
||||
target.addTag(BattlerTagType.INTERRUPTED);
|
||||
}
|
||||
|
||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:fallDown", { targetPokemonName: getPokemonNameWithAffix(target) }));
|
||||
return super.apply(user, target, move, args);
|
||||
}
|
||||
}
|
||||
@ -7962,6 +7989,7 @@ const phaseForcedSlower = (phase: MovePhase, target: Pokemon, trickRoom: boolean
|
||||
return phase.isForcedLast() && slower;
|
||||
};
|
||||
|
||||
// TODO: This needs to become unselectable, not merely fail
|
||||
const failOnGravityCondition: MoveConditionFunc = (user, target, move) => !globalScene.arena.getTag(ArenaTagType.GRAVITY);
|
||||
|
||||
const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune();
|
||||
@ -9271,6 +9299,7 @@ export function initMoves() {
|
||||
.attr(RandomMovesetMoveAttr, invalidAssistMoves, true),
|
||||
new SelfStatusMove(MoveId.INGRAIN, PokemonType.GRASS, -1, 20, -1, 0, 3)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.INGRAIN, true, true)
|
||||
// NB: We add the tag directly to avoid removing Telekinesis' accuracy boost
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, true, true)
|
||||
.attr(RemoveBattlerTagAttr, [ BattlerTagType.FLOATING ], true),
|
||||
new AttackMove(MoveId.SUPERPOWER, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 3)
|
||||
@ -9898,10 +9927,10 @@ export function initMoves() {
|
||||
.unimplemented(),
|
||||
new AttackMove(MoveId.SMACK_DOWN, PokemonType.ROCK, MoveCategory.PHYSICAL, 50, 100, 15, -1, 0, 5)
|
||||
.attr(FallDownAttr)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED)
|
||||
.attr(RemoveBattlerTagAttr, [ BattlerTagType.FLYING, BattlerTagType.FLOATING, BattlerTagType.TELEKINESIS ])
|
||||
.attr(HitsTagAttr, BattlerTagType.FLYING)
|
||||
.makesContact(false),
|
||||
.makesContact(false)
|
||||
// TODO: Confirm if Smack Down & Thousand Arrows will ground semi-invulnerable Pokemon with No Guard, etc.
|
||||
.edgeCase(),
|
||||
new AttackMove(MoveId.STORM_THROW, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5)
|
||||
.attr(CritOnlyAttr),
|
||||
new AttackMove(MoveId.FLAME_BURST, PokemonType.FIRE, MoveCategory.SPECIAL, 70, 100, 15, -1, 0, 5)
|
||||
@ -10360,10 +10389,10 @@ export function initMoves() {
|
||||
.attr(FallDownAttr)
|
||||
.attr(HitsTagAttr, BattlerTagType.FLYING)
|
||||
.attr(HitsTagAttr, BattlerTagType.FLOATING)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED)
|
||||
.attr(RemoveBattlerTagAttr, [ BattlerTagType.FLYING, BattlerTagType.FLOATING, BattlerTagType.TELEKINESIS ])
|
||||
.makesContact(false)
|
||||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||
.target(MoveTarget.ALL_NEAR_ENEMIES)
|
||||
// TODO: Confirm if Smack Down & Thousand Arrows will ground semi-invulnerable Pokemon with No Guard, etc.
|
||||
.edgeCase(),
|
||||
new AttackMove(MoveId.THOUSAND_WAVES, PokemonType.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1, 1, true)
|
||||
.makesContact(false)
|
||||
@ -11299,7 +11328,11 @@ export function initMoves() {
|
||||
.attr(ForceSwitchOutAttr, true, SwitchType.SHED_TAIL)
|
||||
.condition(failIfLastInPartyCondition),
|
||||
new SelfStatusMove(MoveId.CHILLY_RECEPTION, PokemonType.ICE, -1, 10, -1, 0, 9)
|
||||
.attr(PreMoveMessageAttr, (user, move) => i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(user) }))
|
||||
.attr(PreMoveMessageAttr, (user, _target, _move) =>
|
||||
// Don't display text if current move phase is follow up (ie move called indirectly)
|
||||
isVirtual((globalScene.phaseManager.getCurrentPhase() as MovePhase).useMode)
|
||||
? ""
|
||||
: i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(user) }))
|
||||
.attr(ChillyReceptionAttr, true),
|
||||
new SelfStatusMove(MoveId.TIDY_UP, PokemonType.NORMAL, -1, 10, -1, 0, 9)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true)
|
||||
|
@ -751,7 +751,7 @@ export async function catchPokemon(
|
||||
UiMode.POKEDEX_PAGE,
|
||||
pokemon.species,
|
||||
pokemon.formIndex,
|
||||
attributes,
|
||||
[attributes],
|
||||
null,
|
||||
() => {
|
||||
globalScene.ui.setMode(UiMode.MESSAGE).then(() => {
|
||||
|
@ -764,7 +764,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
|
||||
readonly subLegendary: boolean;
|
||||
readonly legendary: boolean;
|
||||
readonly mythical: boolean;
|
||||
readonly species: string;
|
||||
public category: string;
|
||||
readonly growthRate: GrowthRate;
|
||||
/** The chance (as a decimal) for this Species to be male, or `null` for genderless species */
|
||||
readonly malePercent: number | null;
|
||||
@ -778,7 +778,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
|
||||
subLegendary: boolean,
|
||||
legendary: boolean,
|
||||
mythical: boolean,
|
||||
species: string,
|
||||
category: string,
|
||||
type1: PokemonType,
|
||||
type2: PokemonType | null,
|
||||
height: number,
|
||||
@ -829,7 +829,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
|
||||
this.subLegendary = subLegendary;
|
||||
this.legendary = legendary;
|
||||
this.mythical = mythical;
|
||||
this.species = species;
|
||||
this.category = category;
|
||||
this.growthRate = growthRate;
|
||||
this.malePercent = malePercent;
|
||||
this.genderDiffs = genderDiffs;
|
||||
@ -968,6 +968,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
|
||||
|
||||
localize(): void {
|
||||
this.name = i18next.t(`pokemon:${SpeciesId[this.speciesId].toLowerCase()}`);
|
||||
this.category = i18next.t(`pokemonCategory:${SpeciesId[this.speciesId].toLowerCase()}_category`);
|
||||
}
|
||||
|
||||
getWildSpeciesForLevel(level: number, allowEvolving: boolean, isBoss: boolean, gameMode: GameMode): SpeciesId {
|
||||
|
@ -261,10 +261,9 @@ export function getTypeDamageMultiplier(attackType: PokemonType, defType: Pokemo
|
||||
return 1;
|
||||
}
|
||||
case PokemonType.STELLAR:
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2228,13 +2228,29 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
return this.teraType;
|
||||
}
|
||||
|
||||
public isGrounded(): boolean {
|
||||
/**
|
||||
* Return whether this Pokemon is currently on the ground.
|
||||
*
|
||||
* To be considered grounded, a Pokemon must either:
|
||||
* * Be {@linkcode GroundedTag | forcibly grounded} from an effect like Smack Down or Ingrain
|
||||
* * Be under the effects of {@linkcode ArenaTagType.GRAVITY | harsh gravity}
|
||||
* * **Not** be any of the following things:
|
||||
* * {@linkcode PokemonType.FLYING | Flying-type}
|
||||
* * {@linkcode Abilities.LEVITATE | Levitating}
|
||||
* * {@linkcode BattlerTagType.FLOATING | Floating} from Magnet Rise, etc.
|
||||
* * Currently {@linkcode SemiInvulnerableTag | semi-invulnerable} with `ignoreSemiInvulnerable` set to `false`
|
||||
* @param ignoreSemiInvulnerable - Whether to ignore the target's semi-invulnerable state when determining groundedness;
|
||||
default `false`
|
||||
* @returns Whether this pokemon is currently grounded, as described above.
|
||||
*/
|
||||
public isGrounded(ignoreSemiInvulnerable = false): boolean {
|
||||
return (
|
||||
!!this.getTag(GroundedTag) ||
|
||||
!!globalScene.arena.hasTag(ArenaTagType.GRAVITY) ||
|
||||
(!this.isOfType(PokemonType.FLYING, true, true) &&
|
||||
!this.hasAbility(AbilityId.LEVITATE) &&
|
||||
!this.getTag(BattlerTagType.FLOATING) &&
|
||||
!this.getTag(SemiInvulnerableTag))
|
||||
(ignoreSemiInvulnerable || !this.getTag(SemiInvulnerableTag)))
|
||||
);
|
||||
}
|
||||
|
||||
@ -2412,7 +2428,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
|
||||
// Handle flying v ground type immunity without removing flying type so effective types are still effective
|
||||
// Related to https://github.com/pagefaultgames/pokerogue/issues/524
|
||||
if (moveType === PokemonType.GROUND && (this.isGrounded() || arena.hasTag(ArenaTagType.GRAVITY))) {
|
||||
//
|
||||
if (moveType === PokemonType.GROUND && this.isGrounded()) {
|
||||
const flyingIndex = types.indexOf(PokemonType.FLYING);
|
||||
if (flyingIndex > -1) {
|
||||
types.splice(flyingIndex, 1);
|
||||
|
@ -668,6 +668,9 @@ export class MovePhase extends BattlePhase {
|
||||
}),
|
||||
500,
|
||||
);
|
||||
|
||||
// Moves with pre-use messages (Magnitude, Chilly Reception, Fickle Beam, etc.) always display their messages even on failure
|
||||
// TODO: This assumes single target for message funcs - is this sustainable?
|
||||
applyMoveAttrs("PreMoveMessageAttr", this.pokemon, this.pokemon.getOpponents(false)[0], this.move.getMove());
|
||||
}
|
||||
|
||||
|
@ -245,6 +245,7 @@ export async function initI18n(): Promise<void> {
|
||||
"pokeball",
|
||||
"pokedexUiHandler",
|
||||
"pokemon",
|
||||
"pokemonCategory",
|
||||
"pokemonEvolutions",
|
||||
"pokemonForm",
|
||||
"pokemonInfo",
|
||||
|
@ -174,6 +174,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
|
||||
private pokemonCaughtHatchedContainer: Phaser.GameObjects.Container;
|
||||
private pokemonCaughtCountText: Phaser.GameObjects.Text;
|
||||
private pokemonFormText: Phaser.GameObjects.Text;
|
||||
private pokemonCategoryText: Phaser.GameObjects.Text;
|
||||
private pokemonHatchedIcon: Phaser.GameObjects.Sprite;
|
||||
private pokemonHatchedCountText: Phaser.GameObjects.Text;
|
||||
private pokemonShinyIcons: Phaser.GameObjects.Sprite[];
|
||||
@ -409,6 +410,12 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
|
||||
this.pokemonFormText.setOrigin(0, 0);
|
||||
this.starterSelectContainer.add(this.pokemonFormText);
|
||||
|
||||
this.pokemonCategoryText = addTextObject(100, 18, "Category", TextStyle.WINDOW_ALT, {
|
||||
fontSize: "42px",
|
||||
});
|
||||
this.pokemonCategoryText.setOrigin(1, 0);
|
||||
this.starterSelectContainer.add(this.pokemonCategoryText);
|
||||
|
||||
this.pokemonCaughtHatchedContainer = globalScene.add.container(2, 25);
|
||||
this.pokemonCaughtHatchedContainer.setScale(0.5);
|
||||
this.starterSelectContainer.add(this.pokemonCaughtHatchedContainer);
|
||||
@ -2354,6 +2361,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
|
||||
this.pokemonCaughtHatchedContainer.setVisible(true);
|
||||
this.pokemonCandyContainer.setVisible(false);
|
||||
this.pokemonFormText.setVisible(false);
|
||||
this.pokemonCategoryText.setVisible(false);
|
||||
|
||||
const defaultDexAttr = globalScene.gameData.getSpeciesDefaultDexAttr(species, true, true);
|
||||
const props = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr);
|
||||
@ -2382,6 +2390,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
|
||||
this.pokemonCaughtHatchedContainer.setVisible(false);
|
||||
this.pokemonCandyContainer.setVisible(false);
|
||||
this.pokemonFormText.setVisible(false);
|
||||
this.pokemonCategoryText.setVisible(false);
|
||||
|
||||
this.setSpeciesDetails(species!, {
|
||||
// TODO: is this bang correct?
|
||||
@ -2534,6 +2543,13 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
|
||||
this.pokemonNameText.setText(species ? "???" : "");
|
||||
}
|
||||
|
||||
// Setting the category
|
||||
if (isFormCaught) {
|
||||
this.pokemonCategoryText.setText(species.category);
|
||||
} else {
|
||||
this.pokemonCategoryText.setText("");
|
||||
}
|
||||
|
||||
// Setting tint of the sprite
|
||||
if (isFormCaught) {
|
||||
this.species.loadAssets(female!, formIndex, shiny, variant as Variant, true).then(() => {
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { PostTurnRestoreBerryAbAttr } from "#app/data/abilities/ability";
|
||||
import type Pokemon from "#app/field/pokemon";
|
||||
import { BerryModifier, PreserveBerryModifier } from "#app/modifier/modifier";
|
||||
import type { ModifierOverride } from "#app/modifier/modifier-type";
|
||||
@ -53,63 +52,59 @@ describe("Abilities - Harvest", () => {
|
||||
.enemyLevel(1)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset([MoveId.SPLASH, MoveId.NUZZLE, MoveId.KNOCK_OFF, MoveId.INCINERATE]);
|
||||
.enemyMoveset([MoveId.SPLASH, MoveId.NUZZLE]);
|
||||
});
|
||||
|
||||
it("replenishes eaten berries", async () => {
|
||||
it("should replenish the user's eaten berries at end of turn", async () => {
|
||||
game.override.startingHeldItems([{ name: "BERRY", type: BerryType.LUM, count: 1 }]);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
await game.move.selectEnemyMove(MoveId.NUZZLE);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
expect(getPlayerBerries()).toHaveLength(0);
|
||||
expectBerriesContaining();
|
||||
expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toHaveLength(1);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expectBerriesContaining({ name: "BERRY", type: BerryType.LUM, count: 1 });
|
||||
expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toEqual([]);
|
||||
});
|
||||
|
||||
it("tracks berries eaten while disabled/not present", async () => {
|
||||
// Note: this also checks for harvest not being present as neutralizing gas works by making
|
||||
// the game consider all other pokemon to *not* have their respective abilities.
|
||||
it("tracks berries eaten while neutralized/not present", async () => {
|
||||
game.override
|
||||
.startingHeldItems([
|
||||
{ name: "BERRY", type: BerryType.ENIGMA, count: 2 },
|
||||
{ name: "BERRY", type: BerryType.LUM, count: 2 },
|
||||
])
|
||||
.enemyAbility(AbilityId.NEUTRALIZING_GAS);
|
||||
.ability(AbilityId.BALL_FETCH);
|
||||
await game.classicMode.startBattle([SpeciesId.MILOTIC]);
|
||||
|
||||
const milotic = game.scene.getPlayerPokemon()!;
|
||||
expect(milotic).toBeDefined();
|
||||
|
||||
// Chug a few berries without harvest (should get tracked)
|
||||
game.move.select(MoveId.SPLASH);
|
||||
await game.move.selectEnemyMove(MoveId.NUZZLE);
|
||||
game.move.use(MoveId.SPLASH);
|
||||
await game.move.forceEnemyMove(MoveId.NUZZLE);
|
||||
await game.toNextTurn();
|
||||
|
||||
const milotic = game.field.getPlayerPokemon();
|
||||
expect(milotic.battleData.berriesEaten).toEqual(expect.arrayContaining([BerryType.ENIGMA, BerryType.LUM]));
|
||||
expect(getPlayerBerries()).toHaveLength(2);
|
||||
|
||||
// Give ourselves harvest and disable enemy neut gas,
|
||||
// but force our roll to fail so we don't accidentally recover anything
|
||||
vi.spyOn(PostTurnRestoreBerryAbAttr.prototype, "canApplyPostTurn").mockReturnValueOnce(false);
|
||||
game.override.ability(AbilityId.HARVEST);
|
||||
game.move.select(MoveId.GASTRO_ACID);
|
||||
await game.move.selectEnemyMove(MoveId.NUZZLE);
|
||||
// Give ourselves harvest, blocked by enemy neut gas
|
||||
game.field.mockAbility(milotic, AbilityId.HARVEST);
|
||||
game.field.mockAbility(game.field.getEnemyPokemon(), AbilityId.NEUTRALIZING_GAS);
|
||||
|
||||
game.move.use(MoveId.SPLASH);
|
||||
await game.move.forceEnemyMove(MoveId.NUZZLE);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(milotic.battleData.berriesEaten).toEqual(
|
||||
expect.arrayContaining([BerryType.ENIGMA, BerryType.LUM, BerryType.ENIGMA, BerryType.LUM]),
|
||||
);
|
||||
expect(getPlayerBerries()).toHaveLength(0);
|
||||
expectBerriesContaining();
|
||||
|
||||
// proc a high roll and we _should_ get a berry back!
|
||||
// Disable neut gas and we _should_ get a berry back!
|
||||
game.field.mockAbility(game.field.getEnemyPokemon(), AbilityId.BALL_FETCH);
|
||||
game.move.select(MoveId.SPLASH);
|
||||
await game.move.selectEnemyMove(MoveId.SPLASH);
|
||||
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(milotic.battleData.berriesEaten).toHaveLength(3);
|
||||
@ -128,7 +123,7 @@ describe("Abilities - Harvest", () => {
|
||||
game.move.select(MoveId.SPLASH);
|
||||
await game.move.selectEnemyMove(MoveId.SPLASH);
|
||||
await game.doKillOpponents();
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
await game.toEndOfTurn();
|
||||
|
||||
// ate 1 berry without recovering (no harvest)
|
||||
expect(regieleki.battleData.berriesEaten).toEqual([BerryType.PETAYA]);
|
||||
@ -142,7 +137,7 @@ describe("Abilities - Harvest", () => {
|
||||
expect(regieleki.getStatStage(Stat.SPATK)).toBe(1);
|
||||
});
|
||||
|
||||
it("keeps harvested berries across reloads", async () => {
|
||||
it("should keep harvested berries across reloads", async () => {
|
||||
game.override
|
||||
.startingHeldItems([{ name: "BERRY", type: BerryType.PETAYA, count: 1 }])
|
||||
.moveset([MoveId.SPLASH, MoveId.EARTHQUAKE])
|
||||
@ -179,7 +174,7 @@ describe("Abilities - Harvest", () => {
|
||||
expect(game.scene.getPlayerPokemon()?.getStatStage(Stat.SPATK)).toBe(1);
|
||||
});
|
||||
|
||||
it("cannot restore capped berries", async () => {
|
||||
it("should not restore capped berries", async () => {
|
||||
const initBerries: ModifierOverride[] = [
|
||||
{ name: "BERRY", type: BerryType.LUM, count: 2 },
|
||||
{ name: "BERRY", type: BerryType.STARF, count: 2 },
|
||||
@ -198,7 +193,7 @@ describe("Abilities - Harvest", () => {
|
||||
// This does nothing on a success (since there'd only be a starf left to grab),
|
||||
// but ensures we don't accidentally let any false positives through.
|
||||
vi.spyOn(Phaser.Math.RND, "integerInRange").mockReturnValue(0);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
await game.toEndOfTurn();
|
||||
|
||||
// recovered a starf
|
||||
expectBerriesContaining(
|
||||
@ -207,7 +202,7 @@ describe("Abilities - Harvest", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("does nothing if all berries are capped", async () => {
|
||||
it("should do nothing if all berries are capped", async () => {
|
||||
const initBerries: ModifierOverride[] = [
|
||||
{ name: "BERRY", type: BerryType.LUM, count: 2 },
|
||||
{ name: "BERRY", type: BerryType.STARF, count: 3 },
|
||||
@ -220,48 +215,41 @@ describe("Abilities - Harvest", () => {
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
await game.move.selectEnemyMove(MoveId.SPLASH);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expectBerriesContaining(...initBerries);
|
||||
});
|
||||
|
||||
describe("move/ability interactions", () => {
|
||||
it("cannot restore incinerated berries", async () => {
|
||||
describe("Move/Ability interactions", () => {
|
||||
it.each<{ name: string; move: MoveId }>([
|
||||
{ name: "Incinerate", move: MoveId.INCINERATE },
|
||||
{ name: "Knock Off", move: MoveId.KNOCK_OFF },
|
||||
])("should not restore berries removed by $name", async ({ move }) => {
|
||||
game.override.startingHeldItems([{ name: "BERRY", type: BerryType.STARF, count: 3 }]);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
await game.move.selectEnemyMove(MoveId.INCINERATE);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
await game.move.forceEnemyMove(move);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toEqual([]);
|
||||
expectBerriesContaining();
|
||||
});
|
||||
|
||||
it("cannot restore knocked off berries", async () => {
|
||||
game.override.startingHeldItems([{ name: "BERRY", type: BerryType.STARF, count: 3 }]);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
await game.move.selectEnemyMove(MoveId.KNOCK_OFF);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toEqual([]);
|
||||
});
|
||||
|
||||
it("can restore berries eaten by Teatime", async () => {
|
||||
it("should restore berries eaten by Teatime", async () => {
|
||||
const initBerries: ModifierOverride[] = [{ name: "BERRY", type: BerryType.STARF, count: 1 }];
|
||||
game.override.startingHeldItems(initBerries).enemyMoveset(MoveId.TEATIME);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
// nom nom the berr berr yay yay
|
||||
game.move.select(MoveId.SPLASH);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toEqual([]);
|
||||
expectBerriesContaining(...initBerries);
|
||||
});
|
||||
|
||||
it("cannot restore Plucked berries for either side", async () => {
|
||||
it("should not restore Plucked berries for either side", async () => {
|
||||
const initBerries: ModifierOverride[] = [{ name: "BERRY", type: BerryType.PETAYA, count: 1 }];
|
||||
game.override.startingHeldItems(initBerries).enemyAbility(AbilityId.HARVEST).enemyMoveset(MoveId.PLUCK);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
@ -273,10 +261,10 @@ describe("Abilities - Harvest", () => {
|
||||
// pluck triggers harvest for neither side
|
||||
expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toEqual([]);
|
||||
expect(game.scene.getEnemyPokemon()?.battleData.berriesEaten).toEqual([]);
|
||||
expect(getPlayerBerries()).toEqual([]);
|
||||
expectBerriesContaining();
|
||||
});
|
||||
|
||||
it("cannot restore berries preserved via Berry Pouch", async () => {
|
||||
it("should not restore berries preserved via Berry Pouch", async () => {
|
||||
// mock berry pouch to have a 100% success rate
|
||||
vi.spyOn(PreserveBerryModifier.prototype, "apply").mockImplementation(
|
||||
(_pokemon: Pokemon, doPreserve: BooleanHolder): boolean => {
|
||||
@ -297,7 +285,7 @@ describe("Abilities - Harvest", () => {
|
||||
expectBerriesContaining(...initBerries);
|
||||
});
|
||||
|
||||
it("can restore stolen berries", async () => {
|
||||
it("should restore berries stolen from another Pokemon", async () => {
|
||||
const initBerries: ModifierOverride[] = [{ name: "BERRY", type: BerryType.SITRUS, count: 1 }];
|
||||
game.override.enemyHeldItems(initBerries).passiveAbility(AbilityId.MAGICIAN).hasPassiveAbility(true);
|
||||
await game.classicMode.startBattle([SpeciesId.MEOWSCARADA]);
|
||||
@ -312,22 +300,23 @@ describe("Abilities - Harvest", () => {
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
expect(player.battleData.berriesEaten).toEqual([BerryType.SITRUS]);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(player.battleData.berriesEaten).toEqual([]);
|
||||
expectBerriesContaining(...initBerries);
|
||||
});
|
||||
|
||||
// TODO: Enable once fling actually works...???
|
||||
it.todo("can restore berries flung at user", async () => {
|
||||
game.override.enemyHeldItems([{ name: "BERRY", type: BerryType.STARF, count: 1 }]).enemyMoveset(MoveId.FLING);
|
||||
it.todo("should restore berries flung at user", async () => {
|
||||
game.override.enemyHeldItems([{ name: "BERRY", type: BerryType.STARF, count: 1 }]);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
await game.move.forceEnemyMove(MoveId.FLING);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toBe([]);
|
||||
expect(getPlayerBerries()).toEqual([]);
|
||||
expectBerriesContaining();
|
||||
});
|
||||
|
||||
// TODO: Enable once Nat Gift gets implemented...???
|
||||
|
@ -1,7 +1,5 @@
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { CommandPhase } from "#app/phases/command-phase";
|
||||
import { TurnEndPhase } from "#app/phases/turn-end-phase";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
@ -25,48 +23,34 @@ describe("Abilities - Pastel Veil", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.battleStyle("double")
|
||||
.moveset([MoveId.TOXIC_THREAD, MoveId.SPLASH])
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemySpecies(SpeciesId.SUNKERN)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
game.override.battleStyle("double").enemyAbility(AbilityId.BALL_FETCH).enemySpecies(SpeciesId.SUNKERN);
|
||||
});
|
||||
|
||||
it("prevents the user and its allies from being afflicted by poison", async () => {
|
||||
it("should prevent the user and its allies from being poisoned", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.GALAR_PONYTA]);
|
||||
const ponyta = game.scene.getPlayerField()[1];
|
||||
const magikarp = game.scene.getPlayerField()[0];
|
||||
ponyta.abilityIndex = 1;
|
||||
const [magikarp, ponyta] = game.scene.getPlayerField();
|
||||
game.field.mockAbility(ponyta, AbilityId.PASTEL_VEIL);
|
||||
|
||||
expect(ponyta.hasAbility(AbilityId.PASTEL_VEIL)).toBe(true);
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
game.move.select(MoveId.TOXIC_THREAD, 1, BattlerIndex.PLAYER);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER);
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.move.forceEnemyMove(MoveId.TOXIC, BattlerIndex.PLAYER);
|
||||
await game.move.forceEnemyMove(MoveId.TOXIC, BattlerIndex.PLAYER_2);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(magikarp.status?.effect).toBeUndefined();
|
||||
});
|
||||
|
||||
it("it heals the poisoned status condition of allies if user is sent out into battle", async () => {
|
||||
it("should heal allies' poison status if user is sent out into battle", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.FEEBAS, SpeciesId.GALAR_PONYTA]);
|
||||
const ponyta = game.scene.getPlayerParty()[2];
|
||||
const magikarp = game.scene.getPlayerField()[0];
|
||||
ponyta.abilityIndex = 1;
|
||||
const [magikarp, , ponyta] = game.scene.getPlayerParty();
|
||||
game.field.mockAbility(ponyta, AbilityId.PASTEL_VEIL);
|
||||
|
||||
expect(ponyta.hasAbility(AbilityId.PASTEL_VEIL)).toBe(true);
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
game.move.select(MoveId.TOXIC_THREAD, 1, BattlerIndex.PLAYER);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
magikarp.trySetStatus(StatusEffect.POISON);
|
||||
expect(magikarp.status?.effect).toBe(StatusEffect.POISON);
|
||||
|
||||
await game.phaseInterceptor.to(CommandPhase);
|
||||
game.move.select(MoveId.SPLASH);
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER);
|
||||
game.doSwitchPokemon(2);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(magikarp.status?.effect).toBeUndefined();
|
||||
});
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { PokemonType } from "#enums/pokemon-type";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { Stat } from "#enums/stat";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
@ -29,15 +27,14 @@ describe("Abilities - Sheer Force", () => {
|
||||
.battleStyle("single")
|
||||
.ability(AbilityId.SHEER_FORCE)
|
||||
.enemySpecies(SpeciesId.ONIX)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset([MoveId.SPLASH])
|
||||
.enemyAbility(AbilityId.NO_GUARD)
|
||||
.enemyMoveset(MoveId.SPLASH)
|
||||
.criticalHits(false);
|
||||
});
|
||||
|
||||
const SHEER_FORCE_MULT = 1.3;
|
||||
|
||||
it("Sheer Force should boost the power of the move but disable secondary effects", async () => {
|
||||
game.override.moveset([MoveId.AIR_SLASH]);
|
||||
it("should boost move power by 1.3x, disabling all secondary effects in the process", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.SHUCKLE]);
|
||||
|
||||
const airSlashMove = allMoves[MoveId.AIR_SLASH];
|
||||
@ -45,110 +42,95 @@ describe("Abilities - Sheer Force", () => {
|
||||
const airSlashFlinchAttr = airSlashMove.getAttrs("FlinchAttr")[0];
|
||||
vi.spyOn(airSlashFlinchAttr, "getMoveChance");
|
||||
|
||||
game.move.select(MoveId.AIR_SLASH);
|
||||
|
||||
game.move.use(MoveId.AIR_SLASH);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.move.forceHit();
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(airSlashMove.calculateBattlePower).toHaveLastReturnedWith(airSlashMove.power * SHEER_FORCE_MULT);
|
||||
expect(airSlashFlinchAttr.getMoveChance).toHaveLastReturnedWith(0);
|
||||
});
|
||||
|
||||
it("Sheer Force does not affect the base damage or secondary effects of binding moves", async () => {
|
||||
game.override.moveset([MoveId.BIND]);
|
||||
it("should affect the base power of binding moves", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.SHUCKLE]);
|
||||
|
||||
const bindMove = allMoves[MoveId.BIND];
|
||||
vi.spyOn(bindMove, "calculateBattlePower");
|
||||
|
||||
game.move.select(MoveId.BIND);
|
||||
|
||||
game.move.use(MoveId.BIND);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.move.forceHit();
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(bindMove.calculateBattlePower).toHaveLastReturnedWith(bindMove.power);
|
||||
});
|
||||
|
||||
it("Sheer Force does not boost the base damage of moves with no secondary effect", async () => {
|
||||
game.override.moveset([MoveId.TACKLE]);
|
||||
it("should not boost power of moves lacking a secondary effect", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.PIDGEOT]);
|
||||
|
||||
const tackleMove = allMoves[MoveId.TACKLE];
|
||||
vi.spyOn(tackleMove, "calculateBattlePower");
|
||||
|
||||
game.move.select(MoveId.TACKLE);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.move.forceHit();
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
game.move.use(MoveId.TACKLE);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(tackleMove.calculateBattlePower).toHaveLastReturnedWith(tackleMove.power);
|
||||
});
|
||||
|
||||
it("Sheer Force can disable the on-hit activation of specific abilities", async () => {
|
||||
game.override
|
||||
.moveset([MoveId.HEADBUTT])
|
||||
.enemySpecies(SpeciesId.SQUIRTLE)
|
||||
.enemyLevel(10)
|
||||
.enemyAbility(AbilityId.COLOR_CHANGE);
|
||||
|
||||
it.each<{ name: string; ability: AbilityId }>([
|
||||
{ name: "Color Change", ability: AbilityId.COLOR_CHANGE },
|
||||
{ name: "Pickpocket", ability: AbilityId.PICKPOCKET },
|
||||
{ name: "Berserk", ability: AbilityId.BERSERK },
|
||||
{ name: "Anger Shell", ability: AbilityId.ANGER_SHELL },
|
||||
{ name: "Wimp Out", ability: AbilityId.WIMP_OUT },
|
||||
{ name: "Emergency Exit", ability: AbilityId.EMERGENCY_EXIT },
|
||||
])("should disable $name on hit when using boosted moves", async ({ ability }) => {
|
||||
game.override.enemySpecies(SpeciesId.SQUIRTLE).enemyLevel(100).enemyAbility(ability);
|
||||
await game.classicMode.startBattle([SpeciesId.PIDGEOT]);
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
enemyPokemon.hp = enemyPokemon.getMaxHp() / 2 + 1; // ensures wimp out works
|
||||
|
||||
const headbuttMove = allMoves[MoveId.HEADBUTT];
|
||||
vi.spyOn(headbuttMove, "calculateBattlePower");
|
||||
const headbuttFlinchAttr = headbuttMove.getAttrs("FlinchAttr")[0];
|
||||
vi.spyOn(headbuttFlinchAttr, "getMoveChance");
|
||||
|
||||
game.move.select(MoveId.HEADBUTT);
|
||||
game.move.use(MoveId.HEADBUTT);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.move.forceHit();
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(enemyPokemon?.getTypes()[0]).toBe(PokemonType.WATER);
|
||||
// ability was disabled when using boosted attack
|
||||
expect(game.field.getEnemyPokemon()).toBe(enemyPokemon); // covers wimp out switch stuff
|
||||
expect(enemyPokemon.waveData.abilitiesApplied).not.toContain(ability);
|
||||
expect(headbuttMove.calculateBattlePower).toHaveLastReturnedWith(headbuttMove.power * SHEER_FORCE_MULT);
|
||||
expect(headbuttFlinchAttr.getMoveChance).toHaveLastReturnedWith(0);
|
||||
});
|
||||
|
||||
it("Two Pokemon with abilities disabled by Sheer Force hitting each other should not cause a crash", async () => {
|
||||
const moveToUse = MoveId.CRUNCH;
|
||||
game.override
|
||||
.enemyAbility(AbilityId.COLOR_CHANGE)
|
||||
.ability(AbilityId.COLOR_CHANGE)
|
||||
.moveset(moveToUse)
|
||||
.enemyMoveset(moveToUse);
|
||||
it("should not crash when 2 pokemon with disabled abilities attack each other", async () => {
|
||||
game.override.enemyAbility(AbilityId.COLOR_CHANGE).ability(AbilityId.COLOR_CHANGE);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.PIDGEOT]);
|
||||
|
||||
const pidgeot = game.scene.getPlayerParty()[0];
|
||||
const onix = game.scene.getEnemyParty()[0];
|
||||
const pidgeot = game.field.getPlayerPokemon();
|
||||
const onix = game.field.getEnemyPokemon();
|
||||
|
||||
pidgeot.stats[Stat.DEF] = 10000;
|
||||
onix.stats[Stat.DEF] = 10000;
|
||||
|
||||
game.move.select(moveToUse);
|
||||
game.move.select(MoveId.CRUNCH);
|
||||
await game.move.forceEnemyMove(MoveId.CRUNCH);
|
||||
await game.toNextTurn();
|
||||
|
||||
// Check that both Pokemon's Color Change activated
|
||||
const expectedTypes = [allMoves[moveToUse].type];
|
||||
const expectedTypes = [allMoves[MoveId.CRUNCH].type];
|
||||
expect(pidgeot.getTypes()).toStrictEqual(expectedTypes);
|
||||
expect(onix.getTypes()).toStrictEqual(expectedTypes);
|
||||
});
|
||||
|
||||
it("Sheer Force should disable Meloetta's transformation from Relic Song", async () => {
|
||||
game.override
|
||||
.ability(AbilityId.SHEER_FORCE)
|
||||
.moveset([MoveId.RELIC_SONG])
|
||||
.enemyMoveset([MoveId.SPLASH])
|
||||
.enemyLevel(100);
|
||||
it("should disable Meloetta's transformation from Relic Song", async () => {
|
||||
game.override.moveset(MoveId.RELIC_SONG);
|
||||
await game.classicMode.startBattle([SpeciesId.MELOETTA]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon();
|
||||
const formKeyStart = playerPokemon?.getFormKey();
|
||||
const playerPokemon = game.field.getPlayerPokemon();
|
||||
const formKeyStart = playerPokemon.getFormKey();
|
||||
|
||||
game.move.select(MoveId.RELIC_SONG);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
await game.toEndOfTurn();
|
||||
expect(formKeyStart).toBe(playerPokemon?.getFormKey());
|
||||
});
|
||||
});
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { BattlerTagType } from "#app/enums/battler-tag-type";
|
||||
import { CommandPhase } from "#app/phases/command-phase";
|
||||
import { TurnEndPhase } from "#app/phases/turn-end-phase";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
@ -25,65 +25,107 @@ describe("Abilities - Sweet Veil", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.battleStyle("double")
|
||||
.moveset([MoveId.SPLASH, MoveId.REST, MoveId.YAWN])
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.POWDER);
|
||||
game.override.battleStyle("double").enemySpecies(SpeciesId.MAGIKARP).enemyAbility(AbilityId.BALL_FETCH);
|
||||
});
|
||||
|
||||
it("prevents the user and its allies from falling asleep", async () => {
|
||||
function expectNoStatus() {
|
||||
game.scene.getPlayerField().forEach(p => expect.soft(p.status?.effect).toBeUndefined());
|
||||
}
|
||||
|
||||
it("should prevent the user and its allies from falling asleep", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.SWIRLIX, SpeciesId.MAGIKARP]);
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
game.move.select(MoveId.SPLASH, 1);
|
||||
game.field.mockAbility(game.field.getPlayerPokemon(), AbilityId.SWEET_VEIL);
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER);
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.move.forceEnemyMove(MoveId.SPORE, BattlerIndex.PLAYER);
|
||||
await game.move.forceEnemyMove(MoveId.SPORE, BattlerIndex.PLAYER_2);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(game.scene.getPlayerField().every(p => p.status?.effect)).toBe(false);
|
||||
expectNoStatus();
|
||||
});
|
||||
|
||||
it("causes Rest to fail when used by the user or its allies", async () => {
|
||||
game.override.enemyMoveset(MoveId.SPLASH);
|
||||
it("should cause Rest to fail when used by the user or its allies, unless the ally has Mold Breaker", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.SWIRLIX, SpeciesId.MAGIKARP]);
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
game.move.select(MoveId.REST, 1);
|
||||
const [swirlix, magikarp] = game.scene.getPlayerField();
|
||||
game.field.mockAbility(swirlix, AbilityId.SWEET_VEIL);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
swirlix.hp = 1;
|
||||
magikarp.hp = 1;
|
||||
game.move.use(MoveId.REST, BattlerIndex.PLAYER);
|
||||
game.move.use(MoveId.REST, BattlerIndex.PLAYER_2);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(game.scene.getPlayerField().every(p => p.status?.effect)).toBe(false);
|
||||
expectNoStatus();
|
||||
expect(swirlix.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
expect(magikarp.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
|
||||
game.field.mockAbility(magikarp, AbilityId.MOLD_BREAKER);
|
||||
game.move.use(MoveId.REST, BattlerIndex.PLAYER);
|
||||
game.move.use(MoveId.REST, BattlerIndex.PLAYER_2);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(magikarp.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
expect(magikarp.hp).toBe(magikarp.getMaxHp());
|
||||
});
|
||||
|
||||
it("causes Yawn to fail if used on the user or its allies", async () => {
|
||||
game.override.enemyMoveset(MoveId.YAWN);
|
||||
it("should cause Yawn to fail if used on the user or its allies", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.SWIRLIX, SpeciesId.MAGIKARP]);
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
game.move.select(MoveId.SPLASH, 1);
|
||||
const [shuckle, swirlix] = game.scene.getPlayerField();
|
||||
game.field.mockAbility(swirlix, AbilityId.SWEET_VEIL);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER);
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.move.forceEnemyMove(MoveId.YAWN, BattlerIndex.PLAYER);
|
||||
await game.move.forceEnemyMove(MoveId.YAWN, BattlerIndex.PLAYER_2);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(game.scene.getPlayerField().every(p => !!p.getTag(BattlerTagType.DROWSY))).toBe(false);
|
||||
expect(shuckle.getTag(BattlerTagType.DROWSY)).toBeUndefined();
|
||||
expect(swirlix.getTag(BattlerTagType.DROWSY)).toBeUndefined();
|
||||
// TODO: Currently ability blockages don't count in terms of failing status moves...??
|
||||
/* const [karp1, karp2] = game.scene.getPlayerField();
|
||||
expect(karp1.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
expect(karp2.getLastXMoves()[0].result).toBe(MoveResult.FAIL); */
|
||||
});
|
||||
|
||||
it("prevents the user and its allies already drowsy due to Yawn from falling asleep.", async () => {
|
||||
game.override.enemySpecies(SpeciesId.PIKACHU).enemyLevel(5).startingLevel(5).enemyMoveset(MoveId.SPLASH);
|
||||
it("should NOT cure allies' sleep status if user is sent out into battle", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.FEEBAS, SpeciesId.SWIRLIX]);
|
||||
const [magikarp, , swirlix] = game.scene.getPlayerParty();
|
||||
game.field.mockAbility(swirlix, AbilityId.PASTEL_VEIL);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.SHUCKLE, SpeciesId.SHUCKLE, SpeciesId.SWIRLIX]);
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
game.move.select(MoveId.YAWN, 1, BattlerIndex.PLAYER);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(game.scene.getPlayerField().some(p => !!p.getTag(BattlerTagType.DROWSY))).toBe(true);
|
||||
|
||||
await game.phaseInterceptor.to(CommandPhase);
|
||||
game.move.select(MoveId.SPLASH);
|
||||
magikarp.trySetStatus(StatusEffect.SLEEP);
|
||||
expect(magikarp.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER);
|
||||
game.doSwitchPokemon(2);
|
||||
|
||||
expect(game.scene.getPlayerField().every(p => p.status?.effect)).toBe(false);
|
||||
expect(magikarp.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
});
|
||||
|
||||
it("should prevent an already-drowsy user or ally from falling asleep", async () => {
|
||||
game.override.ability(AbilityId.BALL_FETCH);
|
||||
await game.classicMode.startBattle([SpeciesId.SHUCKLE, SpeciesId.SWIRLIX]);
|
||||
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER);
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.move.forceEnemyMove(MoveId.YAWN, BattlerIndex.PLAYER);
|
||||
await game.move.forceEnemyMove(MoveId.YAWN, BattlerIndex.PLAYER_2);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
const [shuckle, swirlix, karp1, karp2] = game.scene.getField();
|
||||
expect(shuckle.getTag(BattlerTagType.DROWSY)).toBeDefined();
|
||||
expect(swirlix.getTag(BattlerTagType.DROWSY)).toBeDefined();
|
||||
expect(karp1.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
expect(karp2.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
|
||||
game.field.mockAbility(shuckle, AbilityId.SWEET_VEIL);
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER);
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(shuckle.getTag(BattlerTagType.DROWSY)).toBeDefined();
|
||||
expect(swirlix.getTag(BattlerTagType.DROWSY)).toBeDefined();
|
||||
expectNoStatus();
|
||||
});
|
||||
});
|
||||
|
@ -8,6 +8,8 @@ import { SpeciesId } from "#enums/species-id";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
|
||||
describe("Arena - Gravity", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -27,130 +29,129 @@ describe("Arena - Gravity", () => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.battleStyle("single")
|
||||
.moveset([MoveId.TACKLE, MoveId.GRAVITY, MoveId.FISSURE])
|
||||
.ability(AbilityId.UNNERVE)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemySpecies(SpeciesId.SHUCKLE)
|
||||
.enemyMoveset(MoveId.SPLASH)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyLevel(5);
|
||||
});
|
||||
|
||||
// Reference: https://bulbapedia.bulbagarden.net/wiki/Gravity_(move)
|
||||
|
||||
it("non-OHKO move accuracy is multiplied by 1.67", async () => {
|
||||
const moveToCheck = allMoves[MoveId.TACKLE];
|
||||
|
||||
vi.spyOn(moveToCheck, "calculateBattleAccuracy");
|
||||
|
||||
// Setup Gravity on first turn
|
||||
it("should multiply all non-OHKO move accuracy by 1.67x", async () => {
|
||||
const accSpy = vi.spyOn(allMoves[MoveId.TACKLE], "calculateBattleAccuracy");
|
||||
await game.classicMode.startBattle([SpeciesId.PIKACHU]);
|
||||
game.move.select(MoveId.GRAVITY);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
game.move.use(MoveId.GRAVITY);
|
||||
await game.move.forceEnemyMove(MoveId.TACKLE);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
|
||||
|
||||
// Use non-OHKO move on second turn
|
||||
await game.toNextTurn();
|
||||
game.move.select(MoveId.TACKLE);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(moveToCheck.calculateBattleAccuracy).toHaveLastReturnedWith(100 * 1.67);
|
||||
expect(accSpy).toHaveLastReturnedWith(allMoves[MoveId.TACKLE].accuracy * 1.67);
|
||||
});
|
||||
|
||||
it("OHKO move accuracy is not affected", async () => {
|
||||
/** See Fissure {@link https://bulbapedia.bulbagarden.net/wiki/Fissure_(move)} */
|
||||
const moveToCheck = allMoves[MoveId.FISSURE];
|
||||
|
||||
vi.spyOn(moveToCheck, "calculateBattleAccuracy");
|
||||
|
||||
// Setup Gravity on first turn
|
||||
it("should not affect OHKO move accuracy", async () => {
|
||||
const accSpy = vi.spyOn(allMoves[MoveId.FISSURE], "calculateBattleAccuracy");
|
||||
await game.classicMode.startBattle([SpeciesId.PIKACHU]);
|
||||
game.move.select(MoveId.GRAVITY);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
game.move.use(MoveId.GRAVITY);
|
||||
await game.move.forceEnemyMove(MoveId.FISSURE);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
|
||||
|
||||
// Use OHKO move on second turn
|
||||
await game.toNextTurn();
|
||||
game.move.select(MoveId.FISSURE);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(moveToCheck.calculateBattleAccuracy).toHaveLastReturnedWith(30);
|
||||
expect(accSpy).toHaveLastReturnedWith(allMoves[MoveId.FISSURE].accuracy);
|
||||
});
|
||||
|
||||
describe("Against flying types", () => {
|
||||
it("can be hit by ground-type moves now", async () => {
|
||||
game.override.enemySpecies(SpeciesId.PIDGEOT).moveset([MoveId.GRAVITY, MoveId.EARTHQUAKE]);
|
||||
describe.each<{ name: string; overrides: () => unknown }>([
|
||||
{ name: "Flying-type", overrides: () => game.override.enemySpecies(SpeciesId.MOLTRES) },
|
||||
{ name: "Levitating", overrides: () => game.override.enemyAbility(AbilityId.LEVITATE) },
|
||||
])("should ground $name Pokemon", ({ overrides }) => {
|
||||
beforeEach(overrides);
|
||||
|
||||
it("should remove immunity to Ground-type moves", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.PIKACHU]);
|
||||
|
||||
const pidgeot = game.scene.getEnemyPokemon()!;
|
||||
vi.spyOn(pidgeot, "getAttackTypeEffectiveness");
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
const effectivenessSpy = vi.spyOn(enemy, "getAttackTypeEffectiveness");
|
||||
|
||||
// Try earthquake on 1st turn (fails!);
|
||||
game.move.select(MoveId.EARTHQUAKE);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(pidgeot.getAttackTypeEffectiveness).toHaveLastReturnedWith(0);
|
||||
|
||||
// Setup Gravity on 2nd turn
|
||||
game.move.use(MoveId.EARTHQUAKE);
|
||||
await game.move.forceEnemyMove(MoveId.GRAVITY);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.toNextTurn();
|
||||
game.move.select(MoveId.GRAVITY);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
|
||||
|
||||
// Use ground move on 3rd turn
|
||||
await game.toNextTurn();
|
||||
game.move.select(MoveId.EARTHQUAKE);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(pidgeot.getAttackTypeEffectiveness).toHaveLastReturnedWith(1);
|
||||
expect(effectivenessSpy).toHaveLastReturnedWith(2);
|
||||
expect(enemy.isGrounded()).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps super-effective moves super-effective after using gravity", async () => {
|
||||
game.override.enemySpecies(SpeciesId.PIDGEOT).moveset([MoveId.GRAVITY, MoveId.THUNDERBOLT]);
|
||||
|
||||
it("should preserve normal move effectiveness for secondary type", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.PIKACHU]);
|
||||
|
||||
const pidgeot = game.scene.getEnemyPokemon()!;
|
||||
vi.spyOn(pidgeot, "getAttackTypeEffectiveness");
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
const effectivenessSpy = vi.spyOn(enemy, "getAttackTypeEffectiveness");
|
||||
|
||||
// Setup Gravity on 1st turn
|
||||
game.move.select(MoveId.GRAVITY);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
game.move.use(MoveId.THUNDERBOLT);
|
||||
await game.move.forceEnemyMove(MoveId.GRAVITY);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
|
||||
expect(effectivenessSpy).toHaveLastReturnedWith(2);
|
||||
});
|
||||
|
||||
// Use electric move on 2nd turn
|
||||
it("causes terrain to come into effect", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.PIKACHU]);
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
enemy.hp = 1;
|
||||
const statusSpy = vi.spyOn(enemy, "canSetStatus");
|
||||
|
||||
// Turn 1: set up electric terrain; spore works due to being ungrounded
|
||||
game.move.use(MoveId.SPORE);
|
||||
await game.move.forceEnemyMove(MoveId.ELECTRIC_TERRAIN);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.toNextTurn();
|
||||
game.move.select(MoveId.THUNDERBOLT);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(pidgeot.getAttackTypeEffectiveness).toHaveLastReturnedWith(2);
|
||||
expect(statusSpy).toHaveLastReturnedWith(true);
|
||||
expect(enemy.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
|
||||
enemy.resetStatus();
|
||||
|
||||
// Turn 2: gravity grounds enemy; makes spore fail
|
||||
game.move.use(MoveId.SPORE);
|
||||
await game.move.forceEnemyMove(MoveId.GRAVITY);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(statusSpy).toHaveLastReturnedWith(true);
|
||||
expect(enemy.status?.effect).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("cancels Fly if its user is semi-invulnerable", async () => {
|
||||
game.override.enemySpecies(SpeciesId.SNORLAX).enemyMoveset(MoveId.FLY).moveset([MoveId.GRAVITY, MoveId.SPLASH]);
|
||||
|
||||
it.each<{ name: string; move: MoveId }>([
|
||||
{ name: "Fly", move: MoveId.FLY },
|
||||
{ name: "Bounce", move: MoveId.BOUNCE },
|
||||
{ name: "Sky Drop", move: MoveId.SKY_DROP },
|
||||
])("cancels $name if its user is semi-invulnerable", async ({ move }) => {
|
||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
|
||||
|
||||
const charizard = game.scene.getPlayerPokemon()!;
|
||||
const snorlax = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
const charizard = game.field.getPlayerPokemon();
|
||||
const snorlax = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.use(MoveId.SPLASH);
|
||||
await game.move.forceEnemyMove(move);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(snorlax.getTag(BattlerTagType.FLYING)).toBeDefined();
|
||||
|
||||
game.move.select(MoveId.GRAVITY);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(snorlax.getTag(BattlerTagType.INTERRUPTED)).toBeDefined();
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(charizard.hp).toBe(charizard.getMaxHp());
|
||||
expect(snorlax.getTag(BattlerTagType.FLYING)).toBeUndefined();
|
||||
expect(snorlax.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
});
|
||||
});
|
||||
|
@ -1,69 +0,0 @@
|
||||
import { allMoves } from "#app/data/data-lists";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("Arena - Grassy Terrain", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemyLevel(1)
|
||||
.enemySpecies(SpeciesId.SHUCKLE)
|
||||
.enemyAbility(AbilityId.STURDY)
|
||||
.enemyMoveset(MoveId.FLY)
|
||||
.moveset([MoveId.GRASSY_TERRAIN, MoveId.EARTHQUAKE])
|
||||
.ability(AbilityId.NO_GUARD);
|
||||
});
|
||||
|
||||
it("halves the damage of Earthquake", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.TAUROS]);
|
||||
|
||||
const eq = allMoves[MoveId.EARTHQUAKE];
|
||||
vi.spyOn(eq, "calculateBattlePower");
|
||||
|
||||
game.move.select(MoveId.EARTHQUAKE);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(eq.calculateBattlePower).toHaveReturnedWith(100);
|
||||
|
||||
game.move.select(MoveId.GRASSY_TERRAIN);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(MoveId.EARTHQUAKE);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(eq.calculateBattlePower).toHaveReturnedWith(50);
|
||||
});
|
||||
|
||||
it("Does not halve the damage of Earthquake if opponent is not grounded", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.NINJASK]);
|
||||
|
||||
const eq = allMoves[MoveId.EARTHQUAKE];
|
||||
vi.spyOn(eq, "calculateBattlePower");
|
||||
|
||||
game.move.select(MoveId.GRASSY_TERRAIN);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(MoveId.EARTHQUAKE);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(eq.calculateBattlePower).toHaveReturnedWith(100);
|
||||
});
|
||||
});
|
382
test/arena/terrain.test.ts
Normal file
382
test/arena/terrain.test.ts
Normal file
@ -0,0 +1,382 @@
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { allMoves } from "#app/data/data-lists";
|
||||
import { getTerrainName, TerrainType } from "#app/data/terrain";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import { capitalizeFirstLetter, randSeedInt, toDmgValue } from "#app/utils/common";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { PokemonType } from "#enums/pokemon-type";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import i18next from "i18next";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("Terrain -", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
describe.each<{ terrain: string; type: PokemonType; ability: AbilityId; move: MoveId }>([
|
||||
{ terrain: "Electric", type: PokemonType.ELECTRIC, ability: AbilityId.ELECTRIC_SURGE, move: MoveId.THUNDERBOLT },
|
||||
{ terrain: "Psychic", type: PokemonType.PSYCHIC, ability: AbilityId.PSYCHIC_SURGE, move: MoveId.PSYCHIC },
|
||||
{ terrain: "Grassy", type: PokemonType.GRASS, ability: AbilityId.GRASSY_SURGE, move: MoveId.GIGA_DRAIN },
|
||||
])("Move Power Boosts - $terrain Terrain", ({ type, ability, move }) => {
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.SNOM)
|
||||
.enemyAbility(AbilityId.STURDY)
|
||||
.ability(ability)
|
||||
.passiveAbility(AbilityId.NO_GUARD)
|
||||
.enemyPassiveAbility(AbilityId.LEVITATE);
|
||||
});
|
||||
|
||||
const typeStr = capitalizeFirstLetter(PokemonType[type].toLowerCase());
|
||||
|
||||
it(`should boost grounded uses of ${typeStr}-type moves by 1.3x, even against ungrounded targets`, async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||
|
||||
const powerSpy = vi.spyOn(allMoves[move], "calculateBattlePower");
|
||||
game.move.use(move);
|
||||
await game.move.forceEnemyMove(move);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
// Player grounded attack got boosted while enemy ungrounded attack didn't
|
||||
expect(powerSpy).toHaveLastReturnedWith(allMoves[move].power * 1.3);
|
||||
expect(powerSpy).toHaveNthReturnedWith(1, allMoves[move].power);
|
||||
});
|
||||
|
||||
it(`should change Terrain Pulse into a ${typeStr}-type move and double its base power`, async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||
|
||||
const powerSpy = vi.spyOn(allMoves[MoveId.TERRAIN_PULSE], "calculateBattlePower");
|
||||
const playerTypeSpy = vi.spyOn(game.field.getPlayerPokemon(), "getMoveType");
|
||||
const enemyTypeSpy = vi.spyOn(game.field.getEnemyPokemon(), "getMoveType");
|
||||
|
||||
game.move.use(MoveId.TERRAIN_PULSE);
|
||||
await game.move.forceEnemyMove(MoveId.TERRAIN_PULSE);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
// player grounded terrain pulse was boosted & type converted; enemy ungrounded one wasn't
|
||||
expect(powerSpy).toHaveLastReturnedWith(allMoves[move].power * 2.6); // 2 * 1.3
|
||||
expect(playerTypeSpy).toHaveLastReturnedWith(type);
|
||||
expect(powerSpy).toHaveNthReturnedWith(1, allMoves[move].power);
|
||||
expect(enemyTypeSpy).toHaveNthReturnedWith(1, allMoves[MoveId.TERRAIN_PULSE].type);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Grassy Terrain", () => {
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemyLevel(100)
|
||||
.enemySpecies(SpeciesId.SHUCKLE)
|
||||
.enemyAbility(AbilityId.STURDY)
|
||||
.enemyMoveset(MoveId.GRASSY_TERRAIN)
|
||||
.ability(AbilityId.NO_GUARD);
|
||||
});
|
||||
|
||||
it("should heal all grounded, non-semi-invulnerable pokemon for 1/16th max HP at end of turn", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||
|
||||
// shuckle is grounded, pidgeot isn't
|
||||
const blissey = game.field.getPlayerPokemon();
|
||||
blissey.hp = toDmgValue(blissey.getMaxHp() / 2);
|
||||
expect(blissey.getHpRatio()).toBeCloseTo(0.5, 1);
|
||||
|
||||
const shuckle = game.field.getEnemyPokemon();
|
||||
game.field.mockAbility(shuckle, AbilityId.LEVITATE);
|
||||
shuckle.hp = toDmgValue(shuckle.getMaxHp() / 2);
|
||||
expect(shuckle.getHpRatio()).toBeCloseTo(0.5, 1);
|
||||
|
||||
game.move.use(MoveId.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(game.phaseInterceptor.log).toContain("PokemonHealPhase");
|
||||
expect(blissey.getHpRatio()).toBeCloseTo(0.5625, 1);
|
||||
expect(shuckle.getHpRatio()).toBeCloseTo(0.5, 1);
|
||||
|
||||
game.move.use(MoveId.DIG);
|
||||
await game.toNextTurn();
|
||||
|
||||
// shuckle is airborne and blissey is semi-invulnerable, so nobody gets healed
|
||||
expect(blissey.getHpRatio()).toBeCloseTo(0.5625, 1);
|
||||
expect(shuckle.getHpRatio()).toBeCloseTo(0.5, 1);
|
||||
});
|
||||
|
||||
it.each<{ name: string; move: MoveId; basePower?: number }>([
|
||||
{ name: "Bulldoze", move: MoveId.BULLDOZE },
|
||||
{ name: "Earthquake", move: MoveId.EARTHQUAKE },
|
||||
{ name: "Magnitude", move: MoveId.MAGNITUDE, basePower: 150 }, // magnitude 10
|
||||
])(
|
||||
"should halve $name's base power against grounded, on-field targets",
|
||||
async ({ move, basePower = allMoves[move].power }) => {
|
||||
await game.classicMode.startBattle([SpeciesId.TAUROS]);
|
||||
// force high rolls for guaranteed magnitude 10s
|
||||
vi.fn(randSeedInt).mockReturnValue(100);
|
||||
|
||||
const powerSpy = vi.spyOn(allMoves[move], "calculateBattlePower");
|
||||
|
||||
// Turn 1: attack before grassy terrain set up; 1x
|
||||
game.move.use(move);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(powerSpy).toHaveLastReturnedWith(basePower);
|
||||
|
||||
// Turn 2: attack with grassy terrain already active; 0.5x
|
||||
game.move.use(move);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(powerSpy).toHaveLastReturnedWith(basePower / 2);
|
||||
|
||||
// Turn 3: Give enemy levitate to make ungrounded and attack; 1x
|
||||
game.field.mockAbility(game.field.getEnemyPokemon(), AbilityId.LEVITATE);
|
||||
game.move.use(move);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(powerSpy).toHaveLastReturnedWith(basePower);
|
||||
|
||||
// Turn 4: Remove levitate and make enemy semi-invulnerable; 1x
|
||||
game.field.mockAbility(game.field.getEnemyPokemon(), AbilityId.BALL_FETCH);
|
||||
game.move.use(move);
|
||||
await game.move.forceEnemyMove(MoveId.FLY);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(powerSpy).toHaveLastReturnedWith(basePower);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// TODO: Enable suites after terrain-fail-msg branch is merged
|
||||
describe.skip("Electric Terrain", () => {
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemyLevel(100)
|
||||
.enemySpecies(SpeciesId.SHUCKLE)
|
||||
.enemyAbility(AbilityId.ELECTRIC_SURGE)
|
||||
.ability(AbilityId.NO_GUARD);
|
||||
});
|
||||
|
||||
it("should prevent all grounded pokemon from being put to sleep", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.PIDGEOT]);
|
||||
|
||||
game.move.use(MoveId.SPORE);
|
||||
await game.move.forceEnemyMove(MoveId.SPORE);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
const pidgeot = game.field.getPlayerPokemon();
|
||||
const shuckle = game.field.getEnemyPokemon();
|
||||
expect(pidgeot.status?.effect).toBeUndefined();
|
||||
expect(shuckle.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
// TODO: These don't work due to how move failures are propagated
|
||||
expect(pidgeot.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
expect(shuckle.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
|
||||
expect(game.textInterceptor.logs).not.toContain(
|
||||
i18next.t("terrain:defaultBlockMessage", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(shuckle),
|
||||
terrainName: getTerrainName(TerrainType.ELECTRIC),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should prevent attack moves from applying sleep without showing text/failing move", async () => {
|
||||
vi.spyOn(allMoves[MoveId.RELIC_SONG], "chance", "get").mockReturnValue(100);
|
||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||
|
||||
const shuckle = game.field.getEnemyPokemon();
|
||||
const statusSpy = vi.spyOn(shuckle, "canSetStatus");
|
||||
|
||||
game.move.use(MoveId.RELIC_SONG);
|
||||
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
const blissey = game.field.getPlayerPokemon();
|
||||
expect(shuckle.status?.effect).toBeUndefined();
|
||||
expect(statusSpy).toHaveLastReturnedWith(false);
|
||||
expect(blissey.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
|
||||
expect(game.textInterceptor.logs).not.toContain(
|
||||
i18next.t("terrain:defaultBlockMessage", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(shuckle),
|
||||
terrainName: getTerrainName(TerrainType.ELECTRIC),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip("Misty Terrain", () => {
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemyLevel(100)
|
||||
.enemySpecies(SpeciesId.SHUCKLE)
|
||||
.enemyAbility(AbilityId.MISTY_SURGE)
|
||||
.ability(AbilityId.NO_GUARD);
|
||||
});
|
||||
|
||||
it("should prevent all grounded pokemon from being statused or confused", async () => {
|
||||
game.override.confusionActivation(false); // prevent self hits from ruining things
|
||||
await game.classicMode.startBattle([SpeciesId.PIDGEOT]);
|
||||
|
||||
// shuckle is grounded, pidgeot isn't
|
||||
game.move.use(MoveId.TOXIC);
|
||||
await game.move.forceEnemyMove(MoveId.TOXIC);
|
||||
await game.toNextTurn();
|
||||
|
||||
const pidgeot = game.field.getPlayerPokemon();
|
||||
const shuckle = game.field.getEnemyPokemon();
|
||||
expect(pidgeot.status?.effect).toBeUndefined();
|
||||
expect(shuckle.status?.effect).toBe(StatusEffect.TOXIC);
|
||||
// TODO: These don't work due to how move failures are propagated
|
||||
expect(pidgeot.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
expect(shuckle.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
|
||||
expect(game.textInterceptor.logs).toContain(
|
||||
i18next.t("terrain:mistyBlockMessage", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(shuckle),
|
||||
}),
|
||||
);
|
||||
game.textInterceptor.logs = [];
|
||||
|
||||
game.move.use(MoveId.CONFUSE_RAY);
|
||||
await game.move.forceEnemyMove(MoveId.CONFUSE_RAY);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(pidgeot.getTag(BattlerTagType.CONFUSED)).toBeUndefined();
|
||||
expect(shuckle.getTag(BattlerTagType.CONFUSED)).toBeDefined();
|
||||
});
|
||||
|
||||
it.each<{ status: string; move: MoveId }>([
|
||||
{ status: "Sleep", move: MoveId.RELIC_SONG },
|
||||
{ status: "Burn", move: MoveId.SACRED_FIRE },
|
||||
{ status: "Freeze", move: MoveId.ICE_BEAM },
|
||||
{ status: "Paralysis", move: MoveId.NUZZLE },
|
||||
{ status: "Poison", move: MoveId.SLUDGE_BOMB },
|
||||
{ status: "Toxic", move: MoveId.MALIGNANT_CHAIN },
|
||||
{ status: "Confusion", move: MoveId.MAGICAL_TORQUE },
|
||||
])("should prevent attack moves from applying $name without showing text/failing move", async ({ move }) => {
|
||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||
vi.spyOn(allMoves[move], "chance", "get").mockReturnValue(100);
|
||||
|
||||
game.move.use(move);
|
||||
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
const blissey = game.field.getPlayerPokemon();
|
||||
const shuckle = game.field.getEnemyPokemon();
|
||||
expect(shuckle.status?.effect).toBeUndefined();
|
||||
expect(shuckle.getTag(BattlerTagType.CONFUSED)).toBeUndefined();
|
||||
expect(blissey.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
|
||||
expect(game.textInterceptor.logs).not.toContain(
|
||||
i18next.t("terrain:mistyBlockMessage", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(shuckle),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Psychic Terrain", () => {
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemyLevel(100)
|
||||
.enemySpecies(SpeciesId.SHUCKLE)
|
||||
.enemyAbility(AbilityId.PSYCHIC_SURGE)
|
||||
.enemyPassiveAbility(AbilityId.PRANKSTER)
|
||||
.ability(AbilityId.NO_GUARD)
|
||||
.passiveAbility(AbilityId.GALE_WINGS);
|
||||
});
|
||||
|
||||
it("should block all opponent-targeted priority moves", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||
|
||||
// normal + priority moves
|
||||
game.move.use(MoveId.FAKE_OUT);
|
||||
await game.move.forceEnemyMove(MoveId.FOLLOW_ME);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
const blissey = game.field.getPlayerPokemon();
|
||||
const shuckle = game.field.getEnemyPokemon();
|
||||
expect(blissey.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
expect(shuckle.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
expect(game.textInterceptor.logs).toContain(
|
||||
i18next.t("terrain:defaultBlockMessage", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(shuckle),
|
||||
terrainName: getTerrainName(TerrainType.PSYCHIC),
|
||||
}),
|
||||
);
|
||||
|
||||
// moves that only become priority via ability
|
||||
blissey.hp = 1;
|
||||
game.move.use(MoveId.ROOST);
|
||||
await game.move.forceEnemyMove(MoveId.TICKLE);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(blissey.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
expect(shuckle.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
});
|
||||
|
||||
// TODO: This needs to be fixed
|
||||
it.todo("should not block enemy-targeting spread moves, even if they become priority", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||
|
||||
game.move.use(MoveId.AIR_CUTTER);
|
||||
await game.move.forceEnemyMove(MoveId.SWEET_SCENT);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
const blissey = game.field.getPlayerPokemon();
|
||||
const shuckle = game.field.getEnemyPokemon();
|
||||
expect(blissey.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
expect(shuckle.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
expect(shuckle.hp).toBeLessThan(shuckle.getMaxHp());
|
||||
expect(blissey.getStatStage(Stat.EVA)).toBe(-2);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,11 +1,14 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { RandomMoveAttr } from "#app/data/moves/move";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { AbilityId } from "#app/enums/ability-id";
|
||||
import { WeatherType } from "#enums/weather-type";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import i18next from "i18next";
|
||||
import Phaser from "phaser";
|
||||
//import { TurnInitPhase } from "#app/phases/turn-init-phase";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("Moves - Chilly Reception", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -25,95 +28,121 @@ describe("Moves - Chilly Reception", () => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.battleStyle("single")
|
||||
.moveset([MoveId.CHILLY_RECEPTION, MoveId.SNOWSCAPE])
|
||||
.moveset([MoveId.CHILLY_RECEPTION, MoveId.SNOWSCAPE, MoveId.SPLASH, MoveId.METRONOME])
|
||||
.enemyMoveset(MoveId.SPLASH)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.ability(AbilityId.BALL_FETCH);
|
||||
});
|
||||
|
||||
it("should still change the weather if user can't switch out", async () => {
|
||||
it("should display message before use, switch the user out and change the weather to snow", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.SLOWKING, SpeciesId.MEOWTH]);
|
||||
|
||||
const [slowking, meowth] = game.scene.getPlayerParty();
|
||||
|
||||
game.move.select(MoveId.CHILLY_RECEPTION);
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
|
||||
expect(game.scene.getPlayerPokemon()).toBe(meowth);
|
||||
expect(slowking.isOnField()).toBe(false);
|
||||
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
|
||||
expect(game.textInterceptor.logs).toContain(
|
||||
i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(slowking) }),
|
||||
);
|
||||
});
|
||||
|
||||
it("should still change weather if user can't switch out", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.SLOWKING]);
|
||||
|
||||
game.move.select(MoveId.CHILLY_RECEPTION);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
|
||||
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
|
||||
expect(game.scene.getPlayerPokemon()?.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
});
|
||||
|
||||
it("should switch out even if it's snowing", async () => {
|
||||
it("should still switch out even if weather cannot be changed", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.SLOWKING, SpeciesId.MEOWTH]);
|
||||
// first turn set up snow with snowscape, try chilly reception on second turn
|
||||
|
||||
expect(game.scene.arena.weather?.weatherType).not.toBe(WeatherType.SNOW);
|
||||
|
||||
const [slowking, meowth] = game.scene.getPlayerParty();
|
||||
|
||||
game.move.select(MoveId.SNOWSCAPE);
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
|
||||
|
||||
await game.phaseInterceptor.to("TurnInitPhase", false);
|
||||
game.move.select(MoveId.CHILLY_RECEPTION);
|
||||
game.doSelectPartyPokemon(1);
|
||||
// TODO: Uncomment lines once wimp out PR fixes force switches to not reset summon data immediately
|
||||
// await game.phaseInterceptor.to("SwitchSummonPhase", false);
|
||||
// expect(slowking.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
|
||||
await game.toEndOfTurn();
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
|
||||
expect(game.scene.getPlayerField()[0].species.speciesId).toBe(SpeciesId.MEOWTH);
|
||||
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
|
||||
expect(game.scene.getPlayerPokemon()).toBe(meowth);
|
||||
expect(slowking.isOnField()).toBe(false);
|
||||
});
|
||||
|
||||
it("happy case - switch out and weather changes", async () => {
|
||||
// Source: https://replay.pokemonshowdown.com/gen9ou-2367532550
|
||||
it("should fail (while still displaying message) if neither weather change nor switch out succeeds", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.SLOWKING]);
|
||||
|
||||
expect(game.scene.arena.weather?.weatherType).not.toBe(WeatherType.SNOW);
|
||||
|
||||
const slowking = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(MoveId.SNOWSCAPE);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
|
||||
|
||||
game.move.select(MoveId.CHILLY_RECEPTION);
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
|
||||
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
|
||||
expect(game.scene.getPlayerPokemon()).toBe(slowking);
|
||||
expect(slowking.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
expect(game.textInterceptor.logs).toContain(
|
||||
i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(slowking) }),
|
||||
);
|
||||
});
|
||||
|
||||
it("should succeed without message if called indirectly", async () => {
|
||||
vi.spyOn(RandomMoveAttr.prototype, "getMoveOverride").mockReturnValue(MoveId.CHILLY_RECEPTION);
|
||||
await game.classicMode.startBattle([SpeciesId.SLOWKING, SpeciesId.MEOWTH]);
|
||||
|
||||
game.move.select(MoveId.CHILLY_RECEPTION);
|
||||
game.doSelectPartyPokemon(1);
|
||||
const [slowking, meowth] = game.scene.getPlayerParty();
|
||||
|
||||
game.move.select(MoveId.METRONOME);
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
|
||||
expect(game.scene.getPlayerField()[0].species.speciesId).toBe(SpeciesId.MEOWTH);
|
||||
expect(game.scene.getPlayerPokemon()).toBe(meowth);
|
||||
expect(slowking.isOnField()).toBe(false);
|
||||
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
|
||||
expect(game.textInterceptor.logs).not.toContain(
|
||||
i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(slowking) }),
|
||||
);
|
||||
});
|
||||
|
||||
// enemy uses another move and weather doesn't change
|
||||
it("check case - enemy not selecting chilly reception doesn't change weather ", async () => {
|
||||
game.override.battleStyle("single").enemyMoveset([MoveId.CHILLY_RECEPTION, MoveId.TACKLE]).moveset(MoveId.SPLASH);
|
||||
|
||||
// Bugcheck test for enemy AI bug
|
||||
it("check case - enemy not selecting chilly reception doesn't change weather", async () => {
|
||||
game.override.enemyMoveset([MoveId.CHILLY_RECEPTION, MoveId.TACKLE]);
|
||||
await game.classicMode.startBattle([SpeciesId.SLOWKING, SpeciesId.MEOWTH]);
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
await game.move.selectEnemyMove(MoveId.TACKLE);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
expect(game.scene.arena.weather?.weatherType).toBe(undefined);
|
||||
});
|
||||
|
||||
it("enemy trainer - expected behavior ", async () => {
|
||||
game.override
|
||||
.battleStyle("single")
|
||||
.startingWave(8)
|
||||
.enemyMoveset(MoveId.CHILLY_RECEPTION)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.moveset([MoveId.SPLASH, MoveId.THUNDERBOLT]);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.JOLTEON]);
|
||||
const RIVAL_MAGIKARP1 = game.scene.getEnemyPokemon()?.id;
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
|
||||
expect(game.scene.getEnemyPokemon()?.id !== RIVAL_MAGIKARP1);
|
||||
|
||||
await game.phaseInterceptor.to("TurnInitPhase", false);
|
||||
game.move.select(MoveId.SPLASH);
|
||||
|
||||
// second chilly reception should still switch out
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
|
||||
await game.phaseInterceptor.to("TurnInitPhase", false);
|
||||
expect(game.scene.getEnemyPokemon()?.id === RIVAL_MAGIKARP1);
|
||||
game.move.select(MoveId.THUNDERBOLT);
|
||||
|
||||
// enemy chilly recep move should fail: it's snowing and no option to switch out
|
||||
// no crashing
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
|
||||
await game.phaseInterceptor.to("TurnInitPhase", false);
|
||||
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
|
||||
game.move.select(MoveId.SPLASH);
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
|
||||
expect(game.scene.arena.weather?.weatherType).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { allMoves } from "#app/data/data-lists";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
@ -8,6 +7,9 @@ import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, it, expect, vi } from "vitest";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import type { MoveEffectPhase } from "#app/phases/move-effect-phase";
|
||||
import { HitCheckResult } from "#enums/hit-check-result";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
|
||||
describe("Moves - Telekinesis", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -26,114 +28,129 @@ describe("Moves - Telekinesis", () => {
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.moveset([MoveId.TELEKINESIS, MoveId.TACKLE, MoveId.MUD_SHOT, MoveId.SMACK_DOWN])
|
||||
.battleStyle("single")
|
||||
.enemySpecies(SpeciesId.SNORLAX)
|
||||
.enemyLevel(60)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset([MoveId.SPLASH]);
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("Telekinesis makes the affected vulnerable to most attacking moves regardless of accuracy", async () => {
|
||||
it("should cause opposing non-OHKO moves to always hit the target", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
const enemyOpponent = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(MoveId.TELEKINESIS);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(enemyOpponent.getTag(BattlerTagType.TELEKINESIS)).toBeDefined();
|
||||
expect(enemyOpponent.getTag(BattlerTagType.FLOATING)).toBeDefined();
|
||||
const player = game.field.getPlayerPokemon();
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.use(MoveId.TELEKINESIS);
|
||||
await game.toNextTurn();
|
||||
vi.spyOn(allMoves[MoveId.TACKLE], "accuracy", "get").mockReturnValue(0);
|
||||
game.move.select(MoveId.TACKLE);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(enemyOpponent.isFullHp()).toBe(false);
|
||||
|
||||
expect(enemy.getTag(BattlerTagType.TELEKINESIS)).toBeDefined();
|
||||
expect(enemy.getTag(BattlerTagType.FLOATING)).toBeDefined();
|
||||
|
||||
game.move.use(MoveId.TACKLE);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.move.forceMiss();
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
|
||||
expect(player.getLastXMoves()[0].result).toBe(MoveResult.MISS);
|
||||
});
|
||||
|
||||
it("Telekinesis makes the affected airborne and immune to most Ground-moves", async () => {
|
||||
it("should render the target immune to Ground-moves and terrain", async () => {
|
||||
game.override.ability(AbilityId.ELECTRIC_SURGE);
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
const enemyOpponent = game.scene.getEnemyPokemon()!;
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.select(MoveId.TELEKINESIS);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(enemyOpponent.getTag(BattlerTagType.TELEKINESIS)).toBeDefined();
|
||||
expect(enemyOpponent.getTag(BattlerTagType.FLOATING)).toBeDefined();
|
||||
game.move.use(MoveId.TELEKINESIS);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.use(MoveId.EARTHQUAKE);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
const hitSpy = vi.spyOn(game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase, "hitCheck");
|
||||
|
||||
await game.toNextTurn();
|
||||
vi.spyOn(allMoves[MoveId.MUD_SHOT], "accuracy", "get").mockReturnValue(100);
|
||||
game.move.select(MoveId.MUD_SHOT);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(enemyOpponent.isFullHp()).toBe(true);
|
||||
|
||||
expect(enemy.hp).toBe(enemy.getMaxHp());
|
||||
expect(hitSpy).toHaveLastReturnedWith([HitCheckResult.NO_EFFECT, 0]);
|
||||
|
||||
game.move.use(MoveId.SPORE);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemy.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
});
|
||||
|
||||
it("Telekinesis can still affect Pokemon that have been transformed into invalid Pokemon", async () => {
|
||||
game.override.enemyMoveset(MoveId.TRANSFORM);
|
||||
it("should still affect enemies transformed into invalid Pokemon", async () => {
|
||||
game.override.enemyAbility(AbilityId.IMPOSTER);
|
||||
await game.classicMode.startBattle([SpeciesId.DIGLETT]);
|
||||
|
||||
const enemyOpponent = game.scene.getEnemyPokemon()!;
|
||||
const enemyOpponent = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.use(MoveId.TELEKINESIS);
|
||||
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(MoveId.TELEKINESIS);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(enemyOpponent.getTag(BattlerTagType.TELEKINESIS)).toBeDefined();
|
||||
expect(enemyOpponent.getTag(BattlerTagType.FLOATING)).toBeDefined();
|
||||
expect(enemyOpponent.summonData.speciesForm?.speciesId).toBe(SpeciesId.DIGLETT);
|
||||
});
|
||||
|
||||
it("Moves like Smack Down and 1000 Arrows remove all effects of Telekinesis from the target Pokemon", async () => {
|
||||
it.each<{ name: string; move: MoveId }>([
|
||||
{ name: "Smack Down", move: MoveId.SMACK_DOWN },
|
||||
{ name: "Thousand Arrows", move: MoveId.THOUSAND_ARROWS },
|
||||
])("should be removed when hit by $name", async ({ move }) => {
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
const enemyOpponent = game.scene.getEnemyPokemon()!;
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.select(MoveId.TELEKINESIS);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(enemyOpponent.getTag(BattlerTagType.TELEKINESIS)).toBeDefined();
|
||||
expect(enemyOpponent.getTag(BattlerTagType.FLOATING)).toBeDefined();
|
||||
|
||||
await game.toNextTurn();
|
||||
game.move.select(MoveId.SMACK_DOWN);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(enemyOpponent.getTag(BattlerTagType.TELEKINESIS)).toBeUndefined();
|
||||
expect(enemyOpponent.getTag(BattlerTagType.FLOATING)).toBeUndefined();
|
||||
|
||||
game.move.use(move);
|
||||
await game.toNextTurn();
|
||||
expect(enemy.getTag(BattlerTagType.TELEKINESIS)).toBeUndefined();
|
||||
expect(enemy.getTag(BattlerTagType.FLOATING)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Ingrain will remove the floating effect of Telekinesis, but not the 100% hit", async () => {
|
||||
game.override.enemyMoveset([MoveId.SPLASH, MoveId.INGRAIN]);
|
||||
it("should become grounded when Ingrain is used, but not remove the guaranteed hit effect", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyOpponent = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(MoveId.TELEKINESIS);
|
||||
await game.move.selectEnemyMove(MoveId.SPLASH);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(enemyOpponent.getTag(BattlerTagType.TELEKINESIS)).toBeDefined();
|
||||
expect(enemyOpponent.getTag(BattlerTagType.FLOATING)).toBeDefined();
|
||||
const playerPokemon = game.field.getPlayerPokemon();
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.use(MoveId.TELEKINESIS);
|
||||
await game.toNextTurn();
|
||||
vi.spyOn(allMoves[MoveId.MUD_SHOT], "accuracy", "get").mockReturnValue(0);
|
||||
game.move.select(MoveId.MUD_SHOT);
|
||||
await game.move.selectEnemyMove(MoveId.INGRAIN);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(enemyOpponent.getTag(BattlerTagType.TELEKINESIS)).toBeDefined();
|
||||
expect(enemyOpponent.getTag(BattlerTagType.INGRAIN)).toBeDefined();
|
||||
expect(enemyOpponent.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined();
|
||||
expect(enemyOpponent.getTag(BattlerTagType.FLOATING)).toBeUndefined();
|
||||
|
||||
game.move.use(MoveId.MUD_SHOT);
|
||||
await game.move.forceEnemyMove(MoveId.INGRAIN);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
await game.move.forceMiss();
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemy.getTag(BattlerTagType.TELEKINESIS)).toBeDefined();
|
||||
expect(enemy.getTag(BattlerTagType.INGRAIN)).toBeDefined();
|
||||
expect(enemy.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined();
|
||||
expect(enemy.getTag(BattlerTagType.FLOATING)).toBeUndefined();
|
||||
expect(enemy.isGrounded()).toBe(false);
|
||||
expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
});
|
||||
|
||||
it("should not be baton passed onto a mega gengar", async () => {
|
||||
it("should not be baton passable onto a mega gengar", async () => {
|
||||
game.override
|
||||
.moveset([MoveId.BATON_PASS])
|
||||
.enemyMoveset([MoveId.TELEKINESIS])
|
||||
.starterForms({ [SpeciesId.GENGAR]: 1 });
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.GENGAR]);
|
||||
game.move.select(MoveId.BATON_PASS);
|
||||
|
||||
game.move.use(MoveId.BATON_PASS);
|
||||
game.doSelectPartyPokemon(1);
|
||||
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
expect(game.scene.getPlayerPokemon()!.getTag(BattlerTagType.TELEKINESIS)).toBeUndefined();
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(game.field.getPlayerPokemon().getTag(BattlerTagType.TELEKINESIS)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { AbilityId } from "#app/enums/ability-id";
|
||||
import { BattlerTagType } from "#app/enums/battler-tag-type";
|
||||
import { BerryPhase } from "#app/phases/berry-phase";
|
||||
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
|
||||
import type { MoveEffectPhase } from "#app/phases/move-effect-phase";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("Moves - Thousand Arrows", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -26,64 +26,106 @@ describe("Moves - Thousand Arrows", () => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.battleStyle("single")
|
||||
.enemySpecies(SpeciesId.TOGETIC)
|
||||
.enemySpecies(SpeciesId.EELEKTROSS)
|
||||
.startingLevel(100)
|
||||
.enemyLevel(100)
|
||||
.moveset([MoveId.THOUSAND_ARROWS])
|
||||
.enemyLevel(50)
|
||||
.criticalHits(false)
|
||||
.ability(AbilityId.COMPOUND_EYES)
|
||||
.enemyAbility(AbilityId.STURDY)
|
||||
.moveset(MoveId.THOUSAND_ARROWS)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("move should hit and ground Flying-type targets", async () => {
|
||||
it("should hit and ground Flying-type targets, dealing neutral damage", async () => {
|
||||
game.override.enemySpecies(SpeciesId.ARCHEOPS);
|
||||
await game.classicMode.startBattle([SpeciesId.ILLUMISE]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
const archeops = game.scene.getEnemyPokemon()!;
|
||||
expect(archeops.getTag(BattlerTagType.IGNORE_FLYING)).toBeUndefined();
|
||||
|
||||
game.move.select(MoveId.THOUSAND_ARROWS);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase", false);
|
||||
const hitSpy = vi.spyOn(game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase, "hitCheck");
|
||||
await game.toEndOfTurn();
|
||||
|
||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
// Enemy should not be grounded before move effect is applied
|
||||
expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeUndefined();
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined();
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
||||
expect(hitSpy).toHaveReturnedWith([expect.anything(), 1]);
|
||||
expect(archeops.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined();
|
||||
expect(archeops.isGrounded()).toBe(true);
|
||||
expect(archeops.hp).toBeLessThan(archeops.getMaxHp());
|
||||
});
|
||||
|
||||
it("move should hit and ground targets with Levitate", async () => {
|
||||
game.override.enemySpecies(SpeciesId.SNORLAX).enemyAbility(AbilityId.LEVITATE);
|
||||
|
||||
it("should hit and ground targets with Levitate without affecting effectiveness", async () => {
|
||||
game.override.enemyPassiveAbility(AbilityId.LEVITATE);
|
||||
await game.classicMode.startBattle([SpeciesId.ILLUMISE]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
const eelektross = game.scene.getEnemyPokemon()!;
|
||||
expect(eelektross.getTag(BattlerTagType.IGNORE_FLYING)).toBeUndefined();
|
||||
|
||||
game.move.select(MoveId.THOUSAND_ARROWS);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase", false);
|
||||
const hitSpy = vi.spyOn(game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase, "hitCheck");
|
||||
await game.toEndOfTurn();
|
||||
|
||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
// Enemy should not be grounded before move effect is applied
|
||||
expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeUndefined();
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined();
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
||||
expect(eelektross.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined();
|
||||
expect(eelektross.hp).toBeLessThan(eelektross.getMaxHp());
|
||||
expect(hitSpy).toHaveReturnedWith([expect.anything(), 2]);
|
||||
});
|
||||
|
||||
it("move should hit and ground targets under the effects of Magnet Rise", async () => {
|
||||
game.override.enemySpecies(SpeciesId.SNORLAX);
|
||||
|
||||
it("should hit and ground targets under the effects of Magnet Rise", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.ILLUMISE]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
game.move.select(MoveId.THOUSAND_ARROWS);
|
||||
await game.move.forceEnemyMove(MoveId.MAGNET_RISE);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
|
||||
enemyPokemon.addTag(BattlerTagType.FLOATING, undefined, MoveId.MAGNET_RISE);
|
||||
// ensure magnet rise suceeeded before getting knocked down
|
||||
const eelektross = game.field.getEnemyPokemon();
|
||||
expect(eelektross.getTag(BattlerTagType.FLOATING)).toBeDefined();
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(eelektross.getTag(BattlerTagType.FLOATING)).toBeUndefined();
|
||||
expect(eelektross.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined();
|
||||
expect(eelektross.hp).toBeLessThan(eelektross.getMaxHp());
|
||||
});
|
||||
|
||||
it.each<{ name: string; move: MoveId }>([
|
||||
{ name: "Fly", move: MoveId.FLY },
|
||||
{ name: "Bounce", move: MoveId.BOUNCE },
|
||||
])("should hit through and cancel the target's $name", async ({ move }) => {
|
||||
await game.classicMode.startBattle([SpeciesId.ILLUMISE]);
|
||||
|
||||
game.move.select(MoveId.THOUSAND_ARROWS);
|
||||
await game.move.forceEnemyMove(move);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
// Fly should've worked until hit
|
||||
const eelektross = game.field.getEnemyPokemon();
|
||||
expect(eelektross.getTag(BattlerTagType.FLYING)).toBeDefined();
|
||||
expect(eelektross.getTag(BattlerTagType.IGNORE_FLYING)).toBeUndefined();
|
||||
|
||||
expect(enemyPokemon.getTag(BattlerTagType.FLOATING)).toBeUndefined();
|
||||
expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined();
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
||||
await game.toEndOfTurn();
|
||||
expect(eelektross.getTag(BattlerTagType.FLYING)).toBeUndefined();
|
||||
expect(eelektross.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined();
|
||||
expect(eelektross.hp).toBeLessThan(eelektross.getMaxHp());
|
||||
});
|
||||
|
||||
// TODO: verify behavior
|
||||
it.todo("should not ground semi-invulnerable targets unless already ungrounded", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.ILLUMISE]);
|
||||
|
||||
game.move.select(MoveId.THOUSAND_ARROWS);
|
||||
await game.move.forceEnemyMove(MoveId.DIG);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
const eelektross = game.field.getEnemyPokemon();
|
||||
expect(eelektross.isGrounded()).toBe(false);
|
||||
expect(eelektross.getTag(BattlerTagType.IGNORE_FLYING)).toBeUndefined();
|
||||
expect(eelektross.hp).toBeLessThan(eelektross.getMaxHp());
|
||||
});
|
||||
|
||||
// TODO: Sky drop is currently unimplemented
|
||||
it.todo("should hit midair targets from Sky Drop without interrupting");
|
||||
});
|
||||
|
@ -64,6 +64,7 @@ import { PostGameOverPhase } from "#app/phases/post-game-over-phase";
|
||||
import { RevivalBlessingPhase } from "#app/phases/revival-blessing-phase";
|
||||
|
||||
import type { PhaseClass, PhaseString } from "#app/@types/phase-types";
|
||||
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
|
||||
|
||||
export interface PromptHandler {
|
||||
phaseTarget?: string;
|
||||
@ -143,6 +144,7 @@ export default class PhaseInterceptor {
|
||||
[AttemptRunPhase, this.startPhase],
|
||||
[SelectBiomePhase, this.startPhase],
|
||||
[MysteryEncounterPhase, this.startPhase],
|
||||
[PokemonHealPhase, this.startPhase],
|
||||
[MysteryEncounterOptionSelectedPhase, this.startPhase],
|
||||
[MysteryEncounterBattlePhase, this.startPhase],
|
||||
[MysteryEncounterRewardsPhase, this.startPhase],
|
||||
|
Loading…
Reference in New Issue
Block a user