mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-06-21 00:52:47 +02:00
Merge a519995b47
into 1ff2701964
This commit is contained in:
commit
969189eab2
@ -2235,10 +2235,12 @@ export class MoveTypeChangeAbAttr extends PreAttackAbAttr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Ability attribute for changing a pokemon's type before using a move */
|
/**
|
||||||
|
* Attribute to change the ability holder's type to that of the move being executed.
|
||||||
|
* Used by {@linkcode Abilities.PROTEAN} and {@linkcode Abilities.LIBERO}.
|
||||||
|
*/
|
||||||
export class PokemonTypeChangeAbAttr extends PreAttackAbAttr {
|
export class PokemonTypeChangeAbAttr extends PreAttackAbAttr {
|
||||||
private moveType: PokemonType;
|
private moveType: PokemonType = PokemonType.UNKNOWN;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(true);
|
super(true);
|
||||||
}
|
}
|
||||||
@ -2252,28 +2254,27 @@ export class PokemonTypeChangeAbAttr extends PreAttackAbAttr {
|
|||||||
_args: any[],
|
_args: any[],
|
||||||
): boolean {
|
): boolean {
|
||||||
if (
|
if (
|
||||||
!pokemon.isTerastallized &&
|
pokemon.isTerastallized ||
|
||||||
move.id !== MoveId.STRUGGLE &&
|
move.id === MoveId.STRUGGLE ||
|
||||||
/**
|
/**
|
||||||
* Skip moves that call other moves because these moves generate a following move that will trigger this ability attribute
|
* Skip moves that call other moves because these moves generate a following move that will trigger this ability attribute
|
||||||
* @see {@link https://bulbapedia.bulbagarden.net/wiki/Category:Moves_that_call_other_moves}
|
* @see {@link https://bulbapedia.bulbagarden.net/wiki/Category:Moves_that_call_other_moves}
|
||||||
*/
|
*/
|
||||||
!move.findAttr(
|
move.hasAttr("CallMoveAttr") ||
|
||||||
attr =>
|
move.hasAttr("NaturePowerAttr") // TODO: remove this line when nature power is made to extend from `CallMoveAttr`
|
||||||
attr.is("RandomMovesetMoveAttr") ||
|
|
||||||
attr.is("RandomMoveAttr") ||
|
|
||||||
attr.is("NaturePowerAttr") ||
|
|
||||||
attr.is("CopyMoveAttr"),
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip changing type if we're already of the given type as-is
|
||||||
const moveType = pokemon.getMoveType(move);
|
const moveType = pokemon.getMoveType(move);
|
||||||
if (pokemon.getTypes().some(t => t !== moveType)) {
|
if (pokemon.getTypes().every(t => t === moveType)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
this.moveType = moveType;
|
this.moveType = moveType;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
override applyPreAttack(
|
override applyPreAttack(
|
||||||
pokemon: Pokemon,
|
pokemon: Pokemon,
|
||||||
@ -8485,9 +8486,12 @@ export function initAbilities() {
|
|||||||
new Ability(AbilityId.CHEEK_POUCH, 6)
|
new Ability(AbilityId.CHEEK_POUCH, 6)
|
||||||
.attr(HealFromBerryUseAbAttr, 1 / 3),
|
.attr(HealFromBerryUseAbAttr, 1 / 3),
|
||||||
new Ability(AbilityId.PROTEAN, 6)
|
new Ability(AbilityId.PROTEAN, 6)
|
||||||
.attr(PokemonTypeChangeAbAttr),
|
.attr(PokemonTypeChangeAbAttr)
|
||||||
//.condition((p) => !p.summonData.abilitiesApplied.includes(AbilityId.PROTEAN)), //Gen 9 Implementation
|
// .condition((p) => !p.summonData.abilitiesApplied.includes(Abilities.PROTEAN)) //Gen 9 Implementation
|
||||||
|
// TODO: needs testing on interaction with weather blockage
|
||||||
|
.edgeCase(),
|
||||||
new Ability(AbilityId.FUR_COAT, 6)
|
new Ability(AbilityId.FUR_COAT, 6)
|
||||||
|
// TODO: Should double defense
|
||||||
.attr(ReceivedMoveDamageMultiplierAbAttr, (_target, _user, move) => move.category === MoveCategory.PHYSICAL, 0.5)
|
.attr(ReceivedMoveDamageMultiplierAbAttr, (_target, _user, move) => move.category === MoveCategory.PHYSICAL, 0.5)
|
||||||
.ignorable(),
|
.ignorable(),
|
||||||
new Ability(AbilityId.MAGICIAN, 6)
|
new Ability(AbilityId.MAGICIAN, 6)
|
||||||
@ -8737,8 +8741,10 @@ export function initAbilities() {
|
|||||||
new Ability(AbilityId.DAUNTLESS_SHIELD, 8)
|
new Ability(AbilityId.DAUNTLESS_SHIELD, 8)
|
||||||
.attr(PostSummonStatStageChangeAbAttr, [ Stat.DEF ], 1, true),
|
.attr(PostSummonStatStageChangeAbAttr, [ Stat.DEF ], 1, true),
|
||||||
new Ability(AbilityId.LIBERO, 8)
|
new Ability(AbilityId.LIBERO, 8)
|
||||||
.attr(PokemonTypeChangeAbAttr),
|
.attr(PokemonTypeChangeAbAttr)
|
||||||
//.condition((p) => !p.summonData.abilitiesApplied.includes(AbilityId.LIBERO)), //Gen 9 Implementation
|
//.condition((p) => !p.summonData.abilitiesApplied.includes(Abilities.LIBERO)), //Gen 9 Implementation
|
||||||
|
// TODO: needs testing on interaction with weather blockage
|
||||||
|
.edgeCase(),
|
||||||
new Ability(AbilityId.BALL_FETCH, 8)
|
new Ability(AbilityId.BALL_FETCH, 8)
|
||||||
.attr(FetchBallAbAttr)
|
.attr(FetchBallAbAttr)
|
||||||
.condition(getOncePerBattleCondition(AbilityId.BALL_FETCH)),
|
.condition(getOncePerBattleCondition(AbilityId.BALL_FETCH)),
|
||||||
|
@ -6,7 +6,6 @@ import { CommonAnim } from "#enums/move-anims-common";
|
|||||||
import { CenterOfAttentionTag } from "#app/data/battler-tags";
|
import { CenterOfAttentionTag } from "#app/data/battler-tags";
|
||||||
import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
|
import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
|
||||||
import { applyMoveAttrs } from "#app/data/moves/apply-attrs";
|
import { applyMoveAttrs } from "#app/data/moves/apply-attrs";
|
||||||
import { allMoves } from "#app/data/data-lists";
|
|
||||||
import { MoveFlags } from "#enums/MoveFlags";
|
import { MoveFlags } from "#enums/MoveFlags";
|
||||||
import { SpeciesFormChangePreMoveTrigger } from "#app/data/pokemon-forms/form-change-triggers";
|
import { SpeciesFormChangePreMoveTrigger } from "#app/data/pokemon-forms/form-change-triggers";
|
||||||
import { getStatusEffectActivationText, getStatusEffectHealText } from "#app/data/status-effect";
|
import { getStatusEffectActivationText, getStatusEffectHealText } from "#app/data/status-effect";
|
||||||
@ -35,11 +34,11 @@ export class MovePhase extends BattlePhase {
|
|||||||
protected _move: PokemonMove;
|
protected _move: PokemonMove;
|
||||||
protected _targets: BattlerIndex[];
|
protected _targets: BattlerIndex[];
|
||||||
public readonly useMode: MoveUseMode; // Made public for quash
|
public readonly useMode: MoveUseMode; // Made public for quash
|
||||||
|
/** Whether the current move is forced last (used for Quash). */
|
||||||
protected forcedLast: boolean;
|
protected forcedLast: boolean;
|
||||||
|
/** Whether the current move should fail but still use PP. */
|
||||||
/** Whether the current move should fail but still use PP */
|
|
||||||
protected failed = false;
|
protected failed = false;
|
||||||
/** Whether the current move should cancel and retain PP */
|
/** Whether the current move should fail and retain PP. */
|
||||||
protected cancelled = false;
|
protected cancelled = false;
|
||||||
|
|
||||||
public get pokemon(): Pokemon {
|
public get pokemon(): Pokemon {
|
||||||
@ -164,6 +163,7 @@ export class MovePhase extends BattlePhase {
|
|||||||
this.resolveFinalPreMoveCancellationChecks();
|
this.resolveFinalPreMoveCancellationChecks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cancel, charge or use the move as applicable.
|
||||||
if (this.cancelled || this.failed) {
|
if (this.cancelled || this.failed) {
|
||||||
this.handlePreMoveFailures();
|
this.handlePreMoveFailures();
|
||||||
} else if (this.move.getMove().isChargingMove() && !this.pokemon.getTag(BattlerTagType.CHARGING)) {
|
} else if (this.move.getMove().isChargingMove() && !this.pokemon.getTag(BattlerTagType.CHARGING)) {
|
||||||
@ -284,8 +284,7 @@ export class MovePhase extends BattlePhase {
|
|||||||
protected lapsePreMoveAndMoveTags(): void {
|
protected lapsePreMoveAndMoveTags(): void {
|
||||||
this.pokemon.lapseTags(BattlerTagLapseType.PRE_MOVE);
|
this.pokemon.lapseTags(BattlerTagLapseType.PRE_MOVE);
|
||||||
|
|
||||||
// TODO: does this intentionally happen before the no targets/MoveId.NONE on queue cancellation case is checked?
|
// This intentionally happens before moves without targets are cancelled (Truant takes priority over lack of targets)
|
||||||
// (In other words, check if truant can proc on a move w/o targets)
|
|
||||||
if (!isIgnoreStatus(this.useMode) && this.canMove() && !this.cancelled) {
|
if (!isIgnoreStatus(this.useMode) && this.canMove() && !this.cancelled) {
|
||||||
this.pokemon.lapseTags(BattlerTagLapseType.MOVE);
|
this.pokemon.lapseTags(BattlerTagLapseType.MOVE);
|
||||||
}
|
}
|
||||||
@ -299,44 +298,27 @@ export class MovePhase extends BattlePhase {
|
|||||||
// form changes happen even before we know that the move wll execute.
|
// form changes happen even before we know that the move wll execute.
|
||||||
globalScene.triggerPokemonFormChange(this.pokemon, SpeciesFormChangePreMoveTrigger);
|
globalScene.triggerPokemonFormChange(this.pokemon, SpeciesFormChangePreMoveTrigger);
|
||||||
|
|
||||||
const isDelayedAttack = move.hasAttr("DelayedAttackAttr");
|
// Check the player side arena if another delayed attack is active and hitting the same slot.
|
||||||
if (isDelayedAttack) {
|
if (move.hasAttr("DelayedAttackAttr")) {
|
||||||
// Check the player side arena if future sight is active
|
|
||||||
const futureSightTags = globalScene.arena.findTags(t => t.tagType === ArenaTagType.FUTURE_SIGHT);
|
|
||||||
const doomDesireTags = globalScene.arena.findTags(t => t.tagType === ArenaTagType.DOOM_DESIRE);
|
|
||||||
let fail = false;
|
|
||||||
const currentTargetIndex = targets[0].getBattlerIndex();
|
const currentTargetIndex = targets[0].getBattlerIndex();
|
||||||
for (const tag of futureSightTags) {
|
const delayedAttackHittingSameSlot = globalScene.arena.tags.some(
|
||||||
if ((tag as DelayedAttackTag).targetIndex === currentTargetIndex) {
|
tag =>
|
||||||
fail = true;
|
(tag.tagType === ArenaTagType.FUTURE_SIGHT || tag.tagType === ArenaTagType.DOOM_DESIRE) &&
|
||||||
break;
|
(tag as DelayedAttackTag).targetIndex === currentTargetIndex,
|
||||||
}
|
);
|
||||||
}
|
|
||||||
for (const tag of doomDesireTags) {
|
if (delayedAttackHittingSameSlot) {
|
||||||
if ((tag as DelayedAttackTag).targetIndex === currentTargetIndex) {
|
this.failMove(true);
|
||||||
fail = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (fail) {
|
|
||||||
this.showMoveText();
|
|
||||||
this.showFailedText();
|
|
||||||
this.end();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let success = true;
|
// Check if the move has any attributes that can interrupt its own use **before** displaying text.
|
||||||
// Check if there are any attributes that can interrupt the move, overriding the fail message.
|
applyMoveAttrs("PreUseInterruptAttr", this.pokemon, targets[0], move);
|
||||||
for (const move of this.move.getMove().getAttrs("PreUseInterruptAttr")) {
|
let failed = move.getAttrs("PreUseInterruptAttr").some(attr => attr.apply(this.pokemon, targets[0], move));
|
||||||
if (move.apply(this.pokemon, targets[0], this.move.getMove())) {
|
if (failed) {
|
||||||
success = false;
|
this.failMove(false);
|
||||||
break;
|
return;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
this.showMoveText();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear out any two turn moves once they've been used.
|
// Clear out any two turn moves once they've been used.
|
||||||
@ -344,6 +326,7 @@ export class MovePhase extends BattlePhase {
|
|||||||
// Move queues should be handled by the calling `CommandPhase` or a manager for it
|
// Move queues should be handled by the calling `CommandPhase` or a manager for it
|
||||||
// @ts-expect-error - useMode is readonly and shouldn't normally be assigned to
|
// @ts-expect-error - useMode is readonly and shouldn't normally be assigned to
|
||||||
this.useMode = moveQueue.shift()?.useMode ?? this.useMode;
|
this.useMode = moveQueue.shift()?.useMode ?? this.useMode;
|
||||||
|
|
||||||
if (this.pokemon.getTag(BattlerTagType.CHARGING)?.sourceMove === this.move.moveId) {
|
if (this.pokemon.getTag(BattlerTagType.CHARGING)?.sourceMove === this.move.moveId) {
|
||||||
this.pokemon.lapseTag(BattlerTagType.CHARGING);
|
this.pokemon.lapseTag(BattlerTagType.CHARGING);
|
||||||
}
|
}
|
||||||
@ -351,52 +334,46 @@ export class MovePhase extends BattlePhase {
|
|||||||
if (!isIgnorePP(this.useMode)) {
|
if (!isIgnorePP(this.useMode)) {
|
||||||
// "commit" to using the move, deducting PP.
|
// "commit" to using the move, deducting PP.
|
||||||
const ppUsed = 1 + this.getPpIncreaseFromPressure(targets);
|
const ppUsed = 1 + this.getPpIncreaseFromPressure(targets);
|
||||||
|
|
||||||
this.move.usePp(ppUsed);
|
this.move.usePp(ppUsed);
|
||||||
globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, move, this.move.ppUsed));
|
globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon.id, move, this.move.ppUsed));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if the move is successful (meaning that its damage/effects can be attempted)
|
* Determine if the move is successful (meaning that its damage/effects can be attempted)
|
||||||
* by checking that all of the following are true:
|
* by checking that all of the following are true:
|
||||||
* - Conditional attributes of the move are all met
|
* - Conditional attributes of the move are all met
|
||||||
* - The target's `ForceSwitchOutImmunityAbAttr` is not triggered (see {@linkcode Move.prototype.applyConditions})
|
|
||||||
* - Weather does not block the move
|
* - Weather does not block the move
|
||||||
* - Terrain does not block the move
|
* - Terrain does not block the move
|
||||||
*
|
|
||||||
* TODO: These steps are straightforward, but the implementation below is extremely convoluted.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move conditions assume the move has a single target
|
* Move conditions assume the move has a single target
|
||||||
* TODO: is this sustainable?
|
* TODO: is this sustainable?
|
||||||
*/
|
*/
|
||||||
let failedDueToTerrain = false;
|
const failsConditions = !move.applyConditions(this.pokemon, targets[0], move);
|
||||||
let failedDueToWeather = false;
|
const failedDueToWeather = globalScene.arena.isMoveWeatherCancelled(this.pokemon, move);
|
||||||
if (success) {
|
const failedDueToTerrain = globalScene.arena.isMoveTerrainCancelled(this.pokemon, this.targets, move);
|
||||||
const passesConditions = move.applyConditions(this.pokemon, targets[0], move);
|
failed ||= failsConditions || failedDueToWeather || failedDueToTerrain;
|
||||||
failedDueToWeather = globalScene.arena.isMoveWeatherCancelled(this.pokemon, move);
|
|
||||||
failedDueToTerrain = globalScene.arena.isMoveTerrainCancelled(this.pokemon, this.targets, move);
|
if (failed) {
|
||||||
success = passesConditions && !failedDueToWeather && !failedDueToTerrain;
|
this.failMove(true, failedDueToWeather, failedDueToTerrain);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the battle's "last move" pointer, unless we're currently mimicking a move.
|
this.executeMove();
|
||||||
if (!allMoves[this.move.moveId].hasAttr("CopyMoveAttr")) {
|
|
||||||
// The last move used is unaffected by moves that fail
|
|
||||||
if (success) {
|
|
||||||
globalScene.currentBattle.lastMove = this.move.moveId;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private executeMove() {
|
||||||
* If the move has not failed, trigger ability-based user type changes and then execute it.
|
|
||||||
*
|
|
||||||
* Notably, Roar, Whirlwind, Trick-or-Treat, and Forest's Curse will trigger these type changes even
|
|
||||||
* if the move fails.
|
|
||||||
*/
|
|
||||||
if (success) {
|
|
||||||
const move = this.move.getMove();
|
const move = this.move.getMove();
|
||||||
|
if (!move.hasAttr("CopyMoveAttr") && !isReflected(this.useMode)) {
|
||||||
|
// Update the battle's "last move" pointer unless we're currently mimicking a move or triggering Dancer.
|
||||||
|
// TODO: Research how Copycat interacts with the final attacking turn of Future Sight and co.
|
||||||
|
globalScene.currentBattle.lastMove = move.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// trigger ability-based user type changes, display move text and then execute move effects.
|
||||||
applyPreAttackAbAttrs("PokemonTypeChangeAbAttr", this.pokemon, null, move);
|
applyPreAttackAbAttrs("PokemonTypeChangeAbAttr", this.pokemon, null, move);
|
||||||
|
this.showMoveText();
|
||||||
globalScene.phaseManager.unshiftNew(
|
globalScene.phaseManager.unshiftNew(
|
||||||
"MoveEffectPhase",
|
"MoveEffectPhase",
|
||||||
this.pokemon.getBattlerIndex(),
|
this.pokemon.getBattlerIndex(),
|
||||||
@ -404,55 +381,46 @@ export class MovePhase extends BattlePhase {
|
|||||||
move,
|
move,
|
||||||
this.useMode,
|
this.useMode,
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
if ([MoveId.ROAR, MoveId.WHIRLWIND, MoveId.TRICK_OR_TREAT, MoveId.FORESTS_CURSE].includes(this.move.moveId)) {
|
|
||||||
applyPreAttackAbAttrs("PokemonTypeChangeAbAttr", this.pokemon, null, this.move.getMove());
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pokemon.pushMoveHistory({
|
|
||||||
move: this.move.moveId,
|
|
||||||
targets: this.targets,
|
|
||||||
result: MoveResult.FAIL,
|
|
||||||
useMode: this.useMode,
|
|
||||||
});
|
|
||||||
|
|
||||||
const failureMessage = move.getFailedText(this.pokemon, targets[0], move);
|
|
||||||
let failedText: string | undefined;
|
|
||||||
if (failureMessage) {
|
|
||||||
failedText = failureMessage;
|
|
||||||
} else if (failedDueToTerrain) {
|
|
||||||
failedText = getTerrainBlockMessage(targets[0], globalScene.arena.getTerrainType());
|
|
||||||
} else if (failedDueToWeather) {
|
|
||||||
failedText = getWeatherBlockMessage(globalScene.arena.getWeatherType());
|
|
||||||
}
|
|
||||||
|
|
||||||
this.showFailedText(failedText);
|
|
||||||
|
|
||||||
// Remove the user from its semi-invulnerable state (if applicable)
|
|
||||||
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle Dancer, which triggers immediately after a move is used (rather than waiting on `this.end()`).
|
// Handle Dancer, which triggers immediately after a move is used (rather than waiting on `this.end()`).
|
||||||
// Note the MoveUseMode check here prevents an infinite Dancer loop.
|
// Note the MoveUseMode check here prevents an infinite Dancer loop.
|
||||||
const dancerModes: MoveUseMode[] = [MoveUseMode.INDIRECT, MoveUseMode.REFLECTED] as const;
|
const dancerModes: MoveUseMode[] = [MoveUseMode.INDIRECT, MoveUseMode.REFLECTED] as const;
|
||||||
if (this.move.getMove().hasFlag(MoveFlags.DANCE_MOVE) && !dancerModes.includes(this.useMode)) {
|
if (this.move.getMove().hasFlag(MoveFlags.DANCE_MOVE) && !dancerModes.includes(this.useMode)) {
|
||||||
// TODO: Fix in dancer PR to move to MEP for hit checks
|
|
||||||
globalScene.getField(true).forEach(pokemon => {
|
globalScene.getField(true).forEach(pokemon => {
|
||||||
applyPostMoveUsedAbAttrs("PostMoveUsedAbAttr", pokemon, this.move, this.pokemon, this.targets);
|
applyPostMoveUsedAbAttrs("PostMoveUsedAbAttr", pokemon, this.move, this.pokemon, this.targets);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Queues a {@linkcode MoveChargePhase} for this phase's invoked move. */
|
/**
|
||||||
protected chargeMove() {
|
* Fail the move currently being used.
|
||||||
|
* Handles failure messages, pushing to move history, etc.
|
||||||
|
* @param showText - Whether to show move text when failing the move.
|
||||||
|
* @param failedDueToWeather - Whether the move failed due to weather (default `false`)
|
||||||
|
* @param failedDueToTerrain - Whether the move failed due to terrain (default `false`)
|
||||||
|
*/
|
||||||
|
protected failMove(showText: boolean, failedDueToWeather = false, failedDueToTerrain = false) {
|
||||||
const move = this.move.getMove();
|
const move = this.move.getMove();
|
||||||
const targets = this.getActiveTargetPokemon();
|
const targets = this.getActiveTargetPokemon();
|
||||||
|
|
||||||
this.showMoveText();
|
// NOTE: DO NOT CHANGE THE ORDER OF OPERATIONS HERE.
|
||||||
|
// Protean is supposed to trigger its effects first, _then_ move text is displayed,
|
||||||
|
// _then_ any blockage messages are shown.
|
||||||
|
|
||||||
|
// Roar, Whirlwind, Trick-or-Treat, and Forest's Curse will trigger Protean/Libero
|
||||||
|
// even on failure, as will all moves blocked by terrain.
|
||||||
|
// TODO: Verify if this also applies to primal weather failures?
|
||||||
|
if (
|
||||||
|
failedDueToTerrain ||
|
||||||
|
[MoveId.ROAR, MoveId.WHIRLWIND, MoveId.TRICK_OR_TREAT, MoveId.FORESTS_CURSE].includes(this.move.moveId)
|
||||||
|
) {
|
||||||
|
applyPreAttackAbAttrs("PokemonTypeChangeAbAttr", this.pokemon, null, move);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showText) {
|
||||||
|
this.showMoveText();
|
||||||
|
}
|
||||||
|
|
||||||
// Conditions currently assume single target
|
|
||||||
// TODO: Is this sustainable?
|
|
||||||
if (!move.applyConditions(this.pokemon, targets[0], move)) {
|
|
||||||
this.pokemon.pushMoveHistory({
|
this.pokemon.pushMoveHistory({
|
||||||
move: this.move.moveId,
|
move: this.move.moveId,
|
||||||
targets: this.targets,
|
targets: this.targets,
|
||||||
@ -460,18 +428,39 @@ export class MovePhase extends BattlePhase {
|
|||||||
useMode: this.useMode,
|
useMode: this.useMode,
|
||||||
});
|
});
|
||||||
|
|
||||||
const failureMessage = move.getFailedText(this.pokemon, targets[0], move);
|
// Use move-specific failure messages if present before checking terrain/weather blockage
|
||||||
this.showMoveText();
|
// and falling back to the classic "But it failed!".
|
||||||
this.showFailedText(failureMessage ?? undefined);
|
const failureMessage =
|
||||||
|
move.getFailedText(this.pokemon, targets[0], move) ??
|
||||||
|
(failedDueToTerrain
|
||||||
|
? getTerrainBlockMessage(targets[0], globalScene.arena.getTerrainType())
|
||||||
|
: failedDueToWeather
|
||||||
|
? getWeatherBlockMessage(globalScene.arena.getWeatherType())
|
||||||
|
: i18next.t("battle:attackFailed"));
|
||||||
|
|
||||||
|
this.showFailedText(failureMessage);
|
||||||
|
|
||||||
// Remove the user from its semi-invulnerable state (if applicable)
|
// Remove the user from its semi-invulnerable state (if applicable)
|
||||||
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
|
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queue a {@linkcode MoveChargePhase} for this phase's invoked move.
|
||||||
|
* Does NOT consume PP (occurs on the 2nd strike of the move)
|
||||||
|
*/
|
||||||
|
protected chargeMove() {
|
||||||
|
const move = this.move.getMove();
|
||||||
|
const targets = this.getActiveTargetPokemon();
|
||||||
|
|
||||||
|
if (!move.applyConditions(this.pokemon, targets[0], move)) {
|
||||||
|
this.failMove(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Protean and Libero apply on the charging turn of charge moves
|
// Protean and Libero apply on the charging turn of charge moves, even before showing usage text
|
||||||
applyPreAttackAbAttrs("PokemonTypeChangeAbAttr", this.pokemon, null, this.move.getMove());
|
applyPreAttackAbAttrs("PokemonTypeChangeAbAttr", this.pokemon, null, this.move.getMove());
|
||||||
|
|
||||||
|
this.showMoveText();
|
||||||
globalScene.phaseManager.unshiftNew(
|
globalScene.phaseManager.unshiftNew(
|
||||||
"MoveChargePhase",
|
"MoveChargePhase",
|
||||||
this.pokemon.getBattlerIndex(),
|
this.pokemon.getBattlerIndex(),
|
||||||
@ -482,7 +471,7 @@ export class MovePhase extends BattlePhase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queues a {@linkcode MoveEndPhase} and then ends the phase
|
* Queue a {@linkcode MoveEndPhase} and then end this phase.
|
||||||
*/
|
*/
|
||||||
public end(): void {
|
public end(): void {
|
||||||
globalScene.phaseManager.unshiftNew(
|
globalScene.phaseManager.unshiftNew(
|
||||||
@ -496,7 +485,7 @@ export class MovePhase extends BattlePhase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies PP increasing abilities (currently only {@link AbilityId.PRESSURE Pressure}) if they exist on the target pokemon.
|
* Applies PP increasing abilities (currently only {@linkcode AbilityId.PRESSURE | Pressure}) if they exist on the target pokemon.
|
||||||
* Note that targets must include only active pokemon.
|
* Note that targets must include only active pokemon.
|
||||||
*
|
*
|
||||||
* TODO: This hardcodes the PP increase at 1 per opponent, rather than deferring to the ability.
|
* TODO: This hardcodes the PP increase at 1 per opponent, rather than deferring to the ability.
|
||||||
@ -574,27 +563,31 @@ export class MovePhase extends BattlePhase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Counter-attacking moves pass in `[`{@linkcode BattlerIndex.ATTACKER}`]` into the constructor's `targets` param.
|
* Update the targets of any counter-attacking moves with `[`{@linkcode BattlerIndex.ATTACKER}`]` set
|
||||||
* This function modifies `this.targets` to reflect the actual battler index of the user's last
|
* to reflect the actual battler index of the user's last attacker.
|
||||||
* attacker.
|
|
||||||
*
|
*
|
||||||
* If there is no last attacker, or they are no longer on the field, a message is displayed and the
|
* If there is no last attacker or they are no longer on the field, a message is displayed and the
|
||||||
* move is marked for failure.
|
* move is marked for failure.
|
||||||
*/
|
*/
|
||||||
protected resolveCounterAttackTarget(): void {
|
protected resolveCounterAttackTarget(): void {
|
||||||
if (this.targets.length === 1 && this.targets[0] === BattlerIndex.ATTACKER) {
|
if (this.targets.length !== 1 || this.targets[0] !== BattlerIndex.ATTACKER) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.pokemon.turnData.attacksReceived.length) {
|
if (this.pokemon.turnData.attacksReceived.length) {
|
||||||
this.targets[0] = this.pokemon.turnData.attacksReceived[0].sourceBattlerIndex;
|
this.targets[0] = this.pokemon.turnData.attacksReceived[0].sourceBattlerIndex;
|
||||||
|
|
||||||
// account for metal burst and comeuppance hitting remaining targets in double battles
|
// account for metal burst and comeuppance hitting remaining targets in double battles
|
||||||
// counterattack will redirect to remaining ally if original attacker faints
|
// counterattack will redirect to remaining ally if original attacker faints
|
||||||
if (globalScene.currentBattle.double && this.move.getMove().hasFlag(MoveFlags.REDIRECT_COUNTER)) {
|
if (
|
||||||
if (globalScene.getField()[this.targets[0]].hp === 0) {
|
globalScene.currentBattle.double &&
|
||||||
|
this.move.getMove().hasFlag(MoveFlags.REDIRECT_COUNTER) &&
|
||||||
|
globalScene.getField()[this.targets[0]].hp === 0
|
||||||
|
) {
|
||||||
const opposingField = this.pokemon.isPlayer() ? globalScene.getEnemyField() : globalScene.getPlayerField();
|
const opposingField = this.pokemon.isPlayer() ? globalScene.getEnemyField() : globalScene.getPlayerField();
|
||||||
this.targets[0] = opposingField.find(p => p.hp > 0)?.getBattlerIndex() ?? BattlerIndex.ATTACKER;
|
this.targets[0] = opposingField.find(p => p.hp > 0)?.getBattlerIndex() ?? BattlerIndex.ATTACKER;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (this.targets[0] === BattlerIndex.ATTACKER) {
|
if (this.targets[0] === BattlerIndex.ATTACKER) {
|
||||||
this.fail();
|
this.fail();
|
||||||
@ -602,12 +595,11 @@ export class MovePhase extends BattlePhase {
|
|||||||
this.showFailedText();
|
this.showFailedText();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the case where the move was cancelled or failed:
|
* Handles the case where the move was cancelled or failed:
|
||||||
* - Uses PP if the move failed (not cancelled) and should use PP (failed moves are not affected by {@link AbilityId.PRESSURE Pressure})
|
* - Uses PP if the move failed (not cancelled) and should use PP (failed moves are not affected by {@linkcode AbilityId.PRESSURE | Pressure})
|
||||||
* - Records a cancelled OR failed move in move history, so abilities like {@link AbilityId.TRUANT Truant} don't trigger on the
|
* - Records a cancelled OR failed move in move history, so abilities like {@linkcode AbilityId.TRUANT | Truant} don't trigger on the
|
||||||
* next turn and soft-lock.
|
* next turn and soft-lock.
|
||||||
* - Lapses `MOVE_EFFECT` tags:
|
* - Lapses `MOVE_EFFECT` tags:
|
||||||
* - Semi-invulnerable battler tags (Fly/Dive/etc.) are intended to lapse on move effects, but also need
|
* - Semi-invulnerable battler tags (Fly/Dive/etc.) are intended to lapse on move effects, but also need
|
||||||
@ -615,18 +607,17 @@ export class MovePhase extends BattlePhase {
|
|||||||
*
|
*
|
||||||
* TODO: ...this seems weird.
|
* TODO: ...this seems weird.
|
||||||
* - Lapses `AFTER_MOVE` tags:
|
* - Lapses `AFTER_MOVE` tags:
|
||||||
* - This handles the effects of {@link MoveId.SUBSTITUTE Substitute}
|
* - This handles the effects of {@linkcode MoveId.SUBSTITUTE | Substitute}
|
||||||
* - Removes the second turn of charge moves
|
* - Removes the second turn of charge moves
|
||||||
*/
|
*/
|
||||||
protected handlePreMoveFailures(): void {
|
protected handlePreMoveFailures(): void {
|
||||||
if (this.cancelled || this.failed) {
|
if (!this.cancelled && !this.failed) {
|
||||||
if (this.failed) {
|
return;
|
||||||
const ppUsed = isIgnorePP(this.useMode) ? 0 : 1;
|
|
||||||
|
|
||||||
if (ppUsed) {
|
|
||||||
this.move.usePp();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.failed) {
|
||||||
|
const ppUsed = isIgnorePP(this.useMode) ? 0 : 1;
|
||||||
|
this.move.usePp(ppUsed);
|
||||||
globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), ppUsed));
|
globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), ppUsed));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -644,23 +635,26 @@ export class MovePhase extends BattlePhase {
|
|||||||
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
|
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
|
||||||
this.pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE);
|
this.pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE);
|
||||||
|
|
||||||
|
// This clears out 2 turn moves after they've been used
|
||||||
|
// TODO: Remove post move queue refactor
|
||||||
this.pokemon.getMoveQueue().shift();
|
this.pokemon.getMoveQueue().shift();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays the move's usage text to the player, unless it's a charge turn (ie: {@link MoveId.SOLAR_BEAM Solar Beam}),
|
* Displays the move's usage text to the player as applicable for the move being used.
|
||||||
* the pokemon is on a recharge turn (ie: {@link MoveId.HYPER_BEAM Hyper Beam}), or a 2-turn move was interrupted (ie: {@link MoveId.FLY Fly}).
|
|
||||||
*/
|
*/
|
||||||
public showMoveText(): void {
|
public showMoveText(): void {
|
||||||
if (this.move.moveId === MoveId.NONE) {
|
// No text for Moves.NONE, recharging/2-turn moves or interrupted moves
|
||||||
return;
|
if (
|
||||||
}
|
this.move.moveId === MoveId.NONE ||
|
||||||
|
this.pokemon.getTag(BattlerTagType.RECHARGING) ||
|
||||||
if (this.pokemon.getTag(BattlerTagType.RECHARGING) || this.pokemon.getTag(BattlerTagType.INTERRUPTED)) {
|
this.pokemon.getTag(BattlerTagType.INTERRUPTED)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Play message for magic coat reflection
|
||||||
|
// TODO: This should be done by the move...
|
||||||
globalScene.phaseManager.queueMessage(
|
globalScene.phaseManager.queueMessage(
|
||||||
i18next.t(isReflected(this.useMode) ? "battle:magicCoatActivated" : "battle:useMove", {
|
i18next.t(isReflected(this.useMode) ? "battle:magicCoatActivated" : "battle:useMove", {
|
||||||
pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon),
|
pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon),
|
||||||
@ -674,7 +668,12 @@ export class MovePhase extends BattlePhase {
|
|||||||
applyMoveAttrs("PreMoveMessageAttr", this.pokemon, this.pokemon.getOpponents(false)[0], this.move.getMove());
|
applyMoveAttrs("PreMoveMessageAttr", this.pokemon, this.pokemon.getOpponents(false)[0], this.move.getMove());
|
||||||
}
|
}
|
||||||
|
|
||||||
public showFailedText(failedText: string = i18next.t("battle:attackFailed")): void {
|
/**
|
||||||
|
* Display the text for a move failing to execute.
|
||||||
|
* @param failedText - The failure text to display; defaults to `"battle:attackFailed"` locale key
|
||||||
|
* ("But it failed!" in english)
|
||||||
|
*/
|
||||||
|
public showFailedText(failedText = i18next.t("battle:attackFailed")): void {
|
||||||
globalScene.phaseManager.queueMessage(failedText);
|
globalScene.phaseManager.queueMessage(failedText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,297 +0,0 @@
|
|||||||
import { allMoves } from "#app/data/data-lists";
|
|
||||||
import { PokemonType } from "#enums/pokemon-type";
|
|
||||||
import { Weather } from "#app/data/weather";
|
|
||||||
import type { PlayerPokemon } from "#app/field/pokemon";
|
|
||||||
import { TurnEndPhase } from "#app/phases/turn-end-phase";
|
|
||||||
import { AbilityId } from "#enums/ability-id";
|
|
||||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
|
||||||
import { BiomeId } from "#enums/biome-id";
|
|
||||||
import { MoveId } from "#enums/move-id";
|
|
||||||
import { SpeciesId } from "#enums/species-id";
|
|
||||||
import { WeatherType } from "#enums/weather-type";
|
|
||||||
import GameManager from "#test/testUtils/gameManager";
|
|
||||||
import Phaser from "phaser";
|
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
||||||
|
|
||||||
describe("Abilities - Libero", () => {
|
|
||||||
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")
|
|
||||||
.ability(AbilityId.LIBERO)
|
|
||||||
.startingLevel(100)
|
|
||||||
.enemySpecies(SpeciesId.RATTATA)
|
|
||||||
.enemyMoveset(MoveId.ENDURE);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ability applies and changes a pokemon's type", async () => {
|
|
||||||
game.override.moveset([MoveId.SPLASH]);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
|
||||||
|
|
||||||
game.move.select(MoveId.SPLASH);
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
testPokemonTypeMatchesDefaultMoveType(leadPokemon, MoveId.SPLASH);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test for Gen9+ functionality, we are using previous funcionality
|
|
||||||
test.skip("ability applies only once per switch in", async () => {
|
|
||||||
game.override.moveset([MoveId.SPLASH, MoveId.AGILITY]);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.BULBASAUR]);
|
|
||||||
|
|
||||||
let leadPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
|
||||||
|
|
||||||
game.move.select(MoveId.SPLASH);
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
testPokemonTypeMatchesDefaultMoveType(leadPokemon, MoveId.SPLASH);
|
|
||||||
|
|
||||||
game.move.select(MoveId.AGILITY);
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
expect(leadPokemon.waveData.abilitiesApplied).toContain(AbilityId.LIBERO);
|
|
||||||
const leadPokemonType = PokemonType[leadPokemon.getTypes()[0]];
|
|
||||||
const moveType = PokemonType[allMoves[MoveId.AGILITY].type];
|
|
||||||
expect(leadPokemonType).not.toBe(moveType);
|
|
||||||
|
|
||||||
await game.toNextTurn();
|
|
||||||
game.doSwitchPokemon(1);
|
|
||||||
await game.toNextTurn();
|
|
||||||
game.doSwitchPokemon(1);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
leadPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
|
||||||
|
|
||||||
game.move.select(MoveId.SPLASH);
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
testPokemonTypeMatchesDefaultMoveType(leadPokemon, MoveId.SPLASH);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ability applies correctly even if the pokemon's move has a variable type", async () => {
|
|
||||||
game.override.moveset([MoveId.WEATHER_BALL]);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
|
||||||
|
|
||||||
game.scene.arena.weather = new Weather(WeatherType.SUNNY);
|
|
||||||
game.move.select(MoveId.WEATHER_BALL);
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
expect(leadPokemon.waveData.abilitiesApplied).toContain(AbilityId.LIBERO);
|
|
||||||
expect(leadPokemon.getTypes()).toHaveLength(1);
|
|
||||||
const leadPokemonType = PokemonType[leadPokemon.getTypes()[0]],
|
|
||||||
moveType = PokemonType[PokemonType.FIRE];
|
|
||||||
expect(leadPokemonType).toBe(moveType);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ability applies correctly even if the type has changed by another ability", async () => {
|
|
||||||
game.override.moveset([MoveId.TACKLE]).passiveAbility(AbilityId.REFRIGERATE);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
|
||||||
|
|
||||||
game.move.select(MoveId.TACKLE);
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
expect(leadPokemon.waveData.abilitiesApplied).toContain(AbilityId.LIBERO);
|
|
||||||
expect(leadPokemon.getTypes()).toHaveLength(1);
|
|
||||||
const leadPokemonType = PokemonType[leadPokemon.getTypes()[0]],
|
|
||||||
moveType = PokemonType[PokemonType.ICE];
|
|
||||||
expect(leadPokemonType).toBe(moveType);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ability applies correctly even if the pokemon's move calls another move", async () => {
|
|
||||||
game.override.moveset([MoveId.NATURE_POWER]);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
|
||||||
|
|
||||||
game.scene.arena.biomeType = BiomeId.MOUNTAIN;
|
|
||||||
game.move.select(MoveId.NATURE_POWER);
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
testPokemonTypeMatchesDefaultMoveType(leadPokemon, MoveId.AIR_SLASH);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ability applies correctly even if the pokemon's move is delayed / charging", async () => {
|
|
||||||
game.override.moveset([MoveId.DIG]);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
|
||||||
|
|
||||||
game.move.select(MoveId.DIG);
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
testPokemonTypeMatchesDefaultMoveType(leadPokemon, MoveId.DIG);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ability applies correctly even if the pokemon's move misses", async () => {
|
|
||||||
game.override.moveset([MoveId.TACKLE]).enemyMoveset(MoveId.SPLASH);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
|
||||||
|
|
||||||
game.move.select(MoveId.TACKLE);
|
|
||||||
await game.move.forceMiss();
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
|
||||||
expect(enemyPokemon.isFullHp()).toBe(true);
|
|
||||||
testPokemonTypeMatchesDefaultMoveType(leadPokemon, MoveId.TACKLE);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ability applies correctly even if the pokemon's move is protected against", async () => {
|
|
||||||
game.override.moveset([MoveId.TACKLE]).enemyMoveset(MoveId.PROTECT);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
|
||||||
|
|
||||||
game.move.select(MoveId.TACKLE);
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
testPokemonTypeMatchesDefaultMoveType(leadPokemon, MoveId.TACKLE);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ability applies correctly even if the pokemon's move fails because of type immunity", async () => {
|
|
||||||
game.override.moveset([MoveId.TACKLE]).enemySpecies(SpeciesId.GASTLY);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
|
||||||
|
|
||||||
game.move.select(MoveId.TACKLE);
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
testPokemonTypeMatchesDefaultMoveType(leadPokemon, MoveId.TACKLE);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ability is not applied if pokemon's type is the same as the move's type", async () => {
|
|
||||||
game.override.moveset([MoveId.SPLASH]);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
|
||||||
|
|
||||||
leadPokemon.summonData.types = [allMoves[MoveId.SPLASH].type];
|
|
||||||
game.move.select(MoveId.SPLASH);
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
expect(leadPokemon.waveData.abilitiesApplied).not.toContain(AbilityId.LIBERO);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ability is not applied if pokemon is terastallized", async () => {
|
|
||||||
game.override.moveset([MoveId.SPLASH]);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
|
||||||
|
|
||||||
leadPokemon.isTerastallized = true;
|
|
||||||
|
|
||||||
game.move.select(MoveId.SPLASH);
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
expect(leadPokemon.waveData.abilitiesApplied).not.toContain(AbilityId.LIBERO);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ability is not applied if pokemon uses struggle", async () => {
|
|
||||||
game.override.moveset([MoveId.STRUGGLE]);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
|
||||||
|
|
||||||
game.move.select(MoveId.STRUGGLE);
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
expect(leadPokemon.waveData.abilitiesApplied).not.toContain(AbilityId.LIBERO);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ability is not applied if the pokemon's move fails", async () => {
|
|
||||||
game.override.moveset([MoveId.BURN_UP]);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
|
||||||
|
|
||||||
game.move.select(MoveId.BURN_UP);
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
expect(leadPokemon.waveData.abilitiesApplied).not.toContain(AbilityId.LIBERO);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ability applies correctly even if the pokemon's Trick-or-Treat fails", async () => {
|
|
||||||
game.override.moveset([MoveId.TRICK_OR_TREAT]).enemySpecies(SpeciesId.GASTLY);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
|
||||||
|
|
||||||
game.move.select(MoveId.TRICK_OR_TREAT);
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
testPokemonTypeMatchesDefaultMoveType(leadPokemon, MoveId.TRICK_OR_TREAT);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ability applies correctly and the pokemon curses itself", async () => {
|
|
||||||
game.override.moveset([MoveId.CURSE]);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
|
||||||
|
|
||||||
game.move.select(MoveId.CURSE);
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
testPokemonTypeMatchesDefaultMoveType(leadPokemon, MoveId.CURSE);
|
|
||||||
expect(leadPokemon.getTag(BattlerTagType.CURSED)).not.toBe(undefined);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function testPokemonTypeMatchesDefaultMoveType(pokemon: PlayerPokemon, move: MoveId) {
|
|
||||||
expect(pokemon.waveData.abilitiesApplied).toContain(AbilityId.LIBERO);
|
|
||||||
expect(pokemon.getTypes()).toHaveLength(1);
|
|
||||||
const pokemonType = PokemonType[pokemon.getTypes()[0]],
|
|
||||||
moveType = PokemonType[allMoves[move].type];
|
|
||||||
expect(pokemonType).toBe(moveType);
|
|
||||||
}
|
|
271
test/abilities/protean-libero.test.ts
Normal file
271
test/abilities/protean-libero.test.ts
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
import { allMoves } from "#app/data/data-lists";
|
||||||
|
import { PokemonType } from "#enums/pokemon-type";
|
||||||
|
import type { PlayerPokemon } from "#app/field/pokemon";
|
||||||
|
import { MoveResult } from "#enums/move-result";
|
||||||
|
import { AbilityId } from "#enums/ability-id";
|
||||||
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
|
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";
|
||||||
|
import { BattleType } from "#enums/battle-type";
|
||||||
|
import { BattlerIndex } from "#enums/battler-index";
|
||||||
|
|
||||||
|
describe("Abilities - Protean/Libero", () => {
|
||||||
|
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")
|
||||||
|
.ability(AbilityId.PROTEAN)
|
||||||
|
.startingLevel(100)
|
||||||
|
.moveset([MoveId.CURSE, MoveId.DIG, MoveId.SPLASH])
|
||||||
|
.enemySpecies(SpeciesId.RATTATA)
|
||||||
|
.enemyMoveset(MoveId.SPLASH);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that the protean/libero ability triggered to change the user's type to
|
||||||
|
* the type of its most recently used move.
|
||||||
|
* Takes into account type overrides from effects.
|
||||||
|
* @param pokemon - The {@linkcode PlayerPokemon} being checked.
|
||||||
|
* @remarks
|
||||||
|
* This will clear the given Pokemon's `abilitiesApplied` set after being called to allow for easier multi-turn testing.
|
||||||
|
*/
|
||||||
|
function expectTypeChange(pokemon: PlayerPokemon) {
|
||||||
|
expect(pokemon.waveData.abilitiesApplied).toContainEqual(expect.toBeOneOf([AbilityId.PROTEAN, AbilityId.LIBERO]));
|
||||||
|
const lastMove = allMoves[pokemon.getLastXMoves()[0].move]!;
|
||||||
|
|
||||||
|
const pokemonTypes = pokemon.getTypes().map(pt => PokemonType[pt]);
|
||||||
|
const moveType = PokemonType[pokemon.getMoveType(lastMove)];
|
||||||
|
expect(pokemonTypes).toEqual([moveType]);
|
||||||
|
pokemon.waveData.abilitiesApplied.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that the protean/libero ability did NOT trigger to change the user's type to
|
||||||
|
* the type of its most recently used move.
|
||||||
|
* Takes into account type overrides from effects.
|
||||||
|
* @param pokemon - The {@linkcode PlayerPokemon} being checked.
|
||||||
|
* @remarks
|
||||||
|
* This will clear the given Pokemon's `abilitiesApplied` set after being called to allow for easier multi-turn testing.
|
||||||
|
*/
|
||||||
|
function expectNoTypeChange(pokemon: PlayerPokemon) {
|
||||||
|
expect(pokemon.waveData.abilitiesApplied).not.toContainEqual(
|
||||||
|
expect.toBeOneOf([AbilityId.PROTEAN, AbilityId.LIBERO]),
|
||||||
|
);
|
||||||
|
const lastMove = allMoves[pokemon.getLastXMoves()[0].move]!;
|
||||||
|
|
||||||
|
const pokemonTypes = pokemon.getTypes().map(pt => PokemonType[pt]);
|
||||||
|
const moveType = PokemonType[pokemon.getMoveType(lastMove, true)];
|
||||||
|
expect(pokemonTypes).not.toEqual([moveType]);
|
||||||
|
pokemon.waveData.abilitiesApplied.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ name: "Protean", ability: AbilityId.PROTEAN },
|
||||||
|
{ name: "Libero", ability: AbilityId.PROTEAN },
|
||||||
|
])("$name should change the user's type to the type of the move being used", async ({ ability }) => {
|
||||||
|
game.override.ability(ability);
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
|
const leadPokemon = game.field.getPlayerPokemon();
|
||||||
|
|
||||||
|
game.move.use(MoveId.SPLASH);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expectTypeChange(leadPokemon);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test for Gen9+ functionality, we are using previous funcionality
|
||||||
|
it.skip("should apply only once per switch in", async () => {
|
||||||
|
game.override.moveset([MoveId.SPLASH, MoveId.AGILITY]);
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.BULBASAUR]);
|
||||||
|
|
||||||
|
const bulbasaur = game.field.getPlayerPokemon();
|
||||||
|
|
||||||
|
game.move.select(MoveId.SPLASH);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expectTypeChange(bulbasaur);
|
||||||
|
|
||||||
|
game.move.select(MoveId.AGILITY);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expectNoTypeChange(bulbasaur);
|
||||||
|
|
||||||
|
// switch out and back in
|
||||||
|
game.doSwitchPokemon(1);
|
||||||
|
await game.toNextTurn();
|
||||||
|
game.doSwitchPokemon(1);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(bulbasaur.isOnField()).toBe(true);
|
||||||
|
|
||||||
|
game.move.select(MoveId.SPLASH);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expectTypeChange(bulbasaur);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each<{ category: string; move?: MoveId; passive?: AbilityId; enemyMove?: MoveId }>([
|
||||||
|
{ category: "Variable type Moves'", move: MoveId.WEATHER_BALL, passive: AbilityId.DROUGHT },
|
||||||
|
{ category: "Type Change Abilities'", passive: AbilityId.REFRIGERATE },
|
||||||
|
{ category: "Move-calling Moves'", move: MoveId.NATURE_POWER, passive: AbilityId.PSYCHIC_SURGE },
|
||||||
|
{ category: "Ion Deluge's", enemyMove: MoveId.ION_DELUGE },
|
||||||
|
{ category: "Electrify's", enemyMove: MoveId.ELECTRIFY },
|
||||||
|
])(
|
||||||
|
"should respect $category final type",
|
||||||
|
async ({ move = MoveId.TACKLE, passive = AbilityId.NONE, enemyMove = MoveId.SPLASH }) => {
|
||||||
|
game.override.passiveAbility(passive);
|
||||||
|
await game.classicMode.startBattle([SpeciesId.LINOONE]); // Pure normal type for move overrides
|
||||||
|
|
||||||
|
const linoone = game.field.getPlayerPokemon();
|
||||||
|
|
||||||
|
game.move.use(move);
|
||||||
|
await game.move.forceEnemyMove(enemyMove);
|
||||||
|
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||||
|
await game.phaseInterceptor.to("BerryPhase"); // NB: berry phase = turn end tags stay = tests happy
|
||||||
|
|
||||||
|
expectTypeChange(linoone);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each<{ cause: string; move?: MoveId; passive?: AbilityId; enemyMove?: MoveId }>([
|
||||||
|
{ cause: "misses", move: MoveId.FOCUS_BLAST },
|
||||||
|
{ cause: "is protected against", enemyMove: MoveId.PROTECT },
|
||||||
|
{ cause: "is ineffective", move: MoveId.EARTHQUAKE },
|
||||||
|
{ cause: "matches only one of its types", move: MoveId.NIGHT_SLASH },
|
||||||
|
{ cause: "is blocked by terrain", move: MoveId.SHADOW_SNEAK, passive: AbilityId.PSYCHIC_SURGE },
|
||||||
|
])(
|
||||||
|
"should still trigger if the user's move $cause",
|
||||||
|
async ({ move = MoveId.TACKLE, passive = AbilityId.NONE, enemyMove = MoveId.SPLASH }) => {
|
||||||
|
game.override.passiveAbility(passive).enemySpecies(SpeciesId.SKARMORY);
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MEOWSCARADA]);
|
||||||
|
|
||||||
|
// FOCUS MISS IS REAL CHAT
|
||||||
|
vi.spyOn(allMoves[MoveId.FOCUS_BLAST], "accuracy", "get").mockReturnValue(0);
|
||||||
|
|
||||||
|
const meow = game.field.getPlayerPokemon();
|
||||||
|
|
||||||
|
game.move.use(move);
|
||||||
|
await game.move.forceEnemyMove(enemyMove);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expectTypeChange(meow);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each<{ cause: string; move?: MoveId; tera?: boolean; passive?: AbilityId }>([
|
||||||
|
{ cause: "user is terastallized to any type", tera: true },
|
||||||
|
{ cause: "user uses Struggle", move: MoveId.STRUGGLE },
|
||||||
|
{ cause: "the user's move is blocked by weather", move: MoveId.FIRE_BLAST, passive: AbilityId.PRIMORDIAL_SEA },
|
||||||
|
{ cause: "the user's move fails", move: MoveId.BURN_UP },
|
||||||
|
])("should not apply if $cause", async ({ move = MoveId.TACKLE, tera = false, passive = AbilityId.NONE }) => {
|
||||||
|
game.override.enemyPassiveAbility(passive);
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
|
const karp = game.field.getPlayerPokemon();
|
||||||
|
|
||||||
|
karp.teraType = PokemonType.STEEL;
|
||||||
|
|
||||||
|
game.move.use(move, BattlerIndex.PLAYER, undefined, tera);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expectNoTypeChange(karp);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not apply if user is already the move's type", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
|
const karp = game.field.getPlayerPokemon();
|
||||||
|
|
||||||
|
game.move.use(MoveId.WATERFALL);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(karp.waveData.abilitiesApplied.size).toBe(0);
|
||||||
|
expect(karp.getTypes()).toEqual([allMoves[MoveId.WATERFALL].type]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each<{ moveName: string; move: MoveId }>([
|
||||||
|
{ moveName: "Roar", move: MoveId.ROAR },
|
||||||
|
{ moveName: "Whirlwind", move: MoveId.WHIRLWIND },
|
||||||
|
{ moveName: "Forest's Curse", move: MoveId.FORESTS_CURSE },
|
||||||
|
{ moveName: "Trick-or-Treat", move: MoveId.TRICK_OR_TREAT },
|
||||||
|
])("should still apply if the user's $moveName fails", async ({ move }) => {
|
||||||
|
game.override.battleType(BattleType.TRAINER).enemySpecies(SpeciesId.TREVENANT); // ghost/grass makes both moves fail
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
|
const leadPokemon = game.field.getPlayerPokemon();
|
||||||
|
|
||||||
|
game.move.use(move);
|
||||||
|
// KO all off-field opponents for Whirlwind and co.
|
||||||
|
for (const enemyMon of game.scene.getEnemyParty()) {
|
||||||
|
if (!enemyMon.isActive()) {
|
||||||
|
enemyMon.hp = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expectTypeChange(leadPokemon);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should trigger on the first turn of charging moves", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
|
const karp = game.field.getPlayerPokemon();
|
||||||
|
|
||||||
|
game.move.select(MoveId.DIG);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expectTypeChange(karp);
|
||||||
|
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
expect(karp.waveData.abilitiesApplied).not.toContain(AbilityId.PROTEAN);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should cause the user to cast Ghost-type Curse on itself", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
|
const karp = game.field.getPlayerPokemon();
|
||||||
|
expect(karp.isOfType(PokemonType.GHOST)).toBe(false);
|
||||||
|
|
||||||
|
game.move.select(MoveId.CURSE);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expectTypeChange(karp);
|
||||||
|
expect(karp.getHpRatio(true)).toBeCloseTo(0.25);
|
||||||
|
expect(karp.getTag(BattlerTagType.CURSED)).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not trigger during Focus Punch's start-of-turn message or being interrupted", async () => {
|
||||||
|
game.override.moveset(MoveId.FOCUS_PUNCH).enemyMoveset(MoveId.ABSORB);
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
|
const karp = game.field.getPlayerPokemon();
|
||||||
|
expect(karp.isOfType(PokemonType.FIGHTING)).toBe(false);
|
||||||
|
|
||||||
|
game.move.select(MoveId.FOCUS_PUNCH);
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to("MessagePhase");
|
||||||
|
expect(karp.isOfType(PokemonType.FIGHTING)).toBe(false);
|
||||||
|
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expectNoTypeChange(karp);
|
||||||
|
expect(karp.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||||
|
});
|
||||||
|
});
|
@ -1,297 +0,0 @@
|
|||||||
import { allMoves } from "#app/data/data-lists";
|
|
||||||
import { PokemonType } from "#enums/pokemon-type";
|
|
||||||
import { Weather } from "#app/data/weather";
|
|
||||||
import type { PlayerPokemon } from "#app/field/pokemon";
|
|
||||||
import { TurnEndPhase } from "#app/phases/turn-end-phase";
|
|
||||||
import { AbilityId } from "#enums/ability-id";
|
|
||||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
|
||||||
import { BiomeId } from "#enums/biome-id";
|
|
||||||
import { MoveId } from "#enums/move-id";
|
|
||||||
import { SpeciesId } from "#enums/species-id";
|
|
||||||
import { WeatherType } from "#enums/weather-type";
|
|
||||||
import GameManager from "#test/testUtils/gameManager";
|
|
||||||
import Phaser from "phaser";
|
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
||||||
|
|
||||||
describe("Abilities - Protean", () => {
|
|
||||||
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")
|
|
||||||
.ability(AbilityId.PROTEAN)
|
|
||||||
.startingLevel(100)
|
|
||||||
.enemySpecies(SpeciesId.RATTATA)
|
|
||||||
.enemyMoveset(MoveId.ENDURE);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ability applies and changes a pokemon's type", async () => {
|
|
||||||
game.override.moveset([MoveId.SPLASH]);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
|
||||||
|
|
||||||
game.move.select(MoveId.SPLASH);
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
testPokemonTypeMatchesDefaultMoveType(leadPokemon, MoveId.SPLASH);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test for Gen9+ functionality, we are using previous funcionality
|
|
||||||
test.skip("ability applies only once per switch in", async () => {
|
|
||||||
game.override.moveset([MoveId.SPLASH, MoveId.AGILITY]);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.BULBASAUR]);
|
|
||||||
|
|
||||||
let leadPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
|
||||||
|
|
||||||
game.move.select(MoveId.SPLASH);
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
testPokemonTypeMatchesDefaultMoveType(leadPokemon, MoveId.SPLASH);
|
|
||||||
|
|
||||||
game.move.select(MoveId.AGILITY);
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
expect(leadPokemon.waveData.abilitiesApplied).toContain(AbilityId.PROTEAN);
|
|
||||||
const leadPokemonType = PokemonType[leadPokemon.getTypes()[0]];
|
|
||||||
const moveType = PokemonType[allMoves[MoveId.AGILITY].type];
|
|
||||||
expect(leadPokemonType).not.toBe(moveType);
|
|
||||||
|
|
||||||
await game.toNextTurn();
|
|
||||||
game.doSwitchPokemon(1);
|
|
||||||
await game.toNextTurn();
|
|
||||||
game.doSwitchPokemon(1);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
leadPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
|
||||||
|
|
||||||
game.move.select(MoveId.SPLASH);
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
testPokemonTypeMatchesDefaultMoveType(leadPokemon, MoveId.SPLASH);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ability applies correctly even if the pokemon's move has a variable type", async () => {
|
|
||||||
game.override.moveset([MoveId.WEATHER_BALL]);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
|
||||||
|
|
||||||
game.scene.arena.weather = new Weather(WeatherType.SUNNY);
|
|
||||||
game.move.select(MoveId.WEATHER_BALL);
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
expect(leadPokemon.waveData.abilitiesApplied).toContain(AbilityId.PROTEAN);
|
|
||||||
expect(leadPokemon.getTypes()).toHaveLength(1);
|
|
||||||
const leadPokemonType = PokemonType[leadPokemon.getTypes()[0]],
|
|
||||||
moveType = PokemonType[PokemonType.FIRE];
|
|
||||||
expect(leadPokemonType).toBe(moveType);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ability applies correctly even if the type has changed by another ability", async () => {
|
|
||||||
game.override.moveset([MoveId.TACKLE]).passiveAbility(AbilityId.REFRIGERATE);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
|
||||||
|
|
||||||
game.move.select(MoveId.TACKLE);
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
expect(leadPokemon.waveData.abilitiesApplied).toContain(AbilityId.PROTEAN);
|
|
||||||
expect(leadPokemon.getTypes()).toHaveLength(1);
|
|
||||||
const leadPokemonType = PokemonType[leadPokemon.getTypes()[0]],
|
|
||||||
moveType = PokemonType[PokemonType.ICE];
|
|
||||||
expect(leadPokemonType).toBe(moveType);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ability applies correctly even if the pokemon's move calls another move", async () => {
|
|
||||||
game.override.moveset([MoveId.NATURE_POWER]);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
|
||||||
|
|
||||||
game.scene.arena.biomeType = BiomeId.MOUNTAIN;
|
|
||||||
game.move.select(MoveId.NATURE_POWER);
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
testPokemonTypeMatchesDefaultMoveType(leadPokemon, MoveId.AIR_SLASH);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ability applies correctly even if the pokemon's move is delayed / charging", async () => {
|
|
||||||
game.override.moveset([MoveId.DIG]);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
|
||||||
|
|
||||||
game.move.select(MoveId.DIG);
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
testPokemonTypeMatchesDefaultMoveType(leadPokemon, MoveId.DIG);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ability applies correctly even if the pokemon's move misses", async () => {
|
|
||||||
game.override.moveset([MoveId.TACKLE]).enemyMoveset(MoveId.SPLASH);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
|
||||||
|
|
||||||
game.move.select(MoveId.TACKLE);
|
|
||||||
await game.move.forceMiss();
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
|
||||||
expect(enemyPokemon.isFullHp()).toBe(true);
|
|
||||||
testPokemonTypeMatchesDefaultMoveType(leadPokemon, MoveId.TACKLE);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ability applies correctly even if the pokemon's move is protected against", async () => {
|
|
||||||
game.override.moveset([MoveId.TACKLE]).enemyMoveset(MoveId.PROTECT);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
|
||||||
|
|
||||||
game.move.select(MoveId.TACKLE);
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
testPokemonTypeMatchesDefaultMoveType(leadPokemon, MoveId.TACKLE);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ability applies correctly even if the pokemon's move fails because of type immunity", async () => {
|
|
||||||
game.override.moveset([MoveId.TACKLE]).enemySpecies(SpeciesId.GASTLY);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
|
||||||
|
|
||||||
game.move.select(MoveId.TACKLE);
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
testPokemonTypeMatchesDefaultMoveType(leadPokemon, MoveId.TACKLE);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ability is not applied if pokemon's type is the same as the move's type", async () => {
|
|
||||||
game.override.moveset([MoveId.SPLASH]);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
|
||||||
|
|
||||||
leadPokemon.summonData.types = [allMoves[MoveId.SPLASH].type];
|
|
||||||
game.move.select(MoveId.SPLASH);
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
expect(leadPokemon.waveData.abilitiesApplied).not.toContain(AbilityId.PROTEAN);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ability is not applied if pokemon is terastallized", async () => {
|
|
||||||
game.override.moveset([MoveId.SPLASH]);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
|
||||||
|
|
||||||
leadPokemon.isTerastallized = true;
|
|
||||||
|
|
||||||
game.move.select(MoveId.SPLASH);
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
expect(leadPokemon.waveData.abilitiesApplied).not.toContain(AbilityId.PROTEAN);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ability is not applied if pokemon uses struggle", async () => {
|
|
||||||
game.override.moveset([MoveId.STRUGGLE]);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
|
||||||
|
|
||||||
game.move.select(MoveId.STRUGGLE);
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
expect(leadPokemon.waveData.abilitiesApplied).not.toContain(AbilityId.PROTEAN);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ability is not applied if the pokemon's move fails", async () => {
|
|
||||||
game.override.moveset([MoveId.BURN_UP]);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
|
||||||
|
|
||||||
game.move.select(MoveId.BURN_UP);
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
expect(leadPokemon.waveData.abilitiesApplied).not.toContain(AbilityId.PROTEAN);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ability applies correctly even if the pokemon's Trick-or-Treat fails", async () => {
|
|
||||||
game.override.moveset([MoveId.TRICK_OR_TREAT]).enemySpecies(SpeciesId.GASTLY);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
|
||||||
|
|
||||||
game.move.select(MoveId.TRICK_OR_TREAT);
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
testPokemonTypeMatchesDefaultMoveType(leadPokemon, MoveId.TRICK_OR_TREAT);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ability applies correctly and the pokemon curses itself", async () => {
|
|
||||||
game.override.moveset([MoveId.CURSE]);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
expect(leadPokemon).not.toBe(undefined);
|
|
||||||
|
|
||||||
game.move.select(MoveId.CURSE);
|
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
testPokemonTypeMatchesDefaultMoveType(leadPokemon, MoveId.CURSE);
|
|
||||||
expect(leadPokemon.getTag(BattlerTagType.CURSED)).not.toBe(undefined);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function testPokemonTypeMatchesDefaultMoveType(pokemon: PlayerPokemon, move: MoveId) {
|
|
||||||
expect(pokemon.waveData.abilitiesApplied).toContain(AbilityId.PROTEAN);
|
|
||||||
expect(pokemon.getTypes()).toHaveLength(1);
|
|
||||||
const pokemonType = PokemonType[pokemon.getTypes()[0]],
|
|
||||||
moveType = PokemonType[allMoves[move].type];
|
|
||||||
expect(pokemonType).toBe(moveType);
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user