mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-09-23 06:53:27 +02:00
Merge ca92df3245
into 8d5ba221d8
This commit is contained in:
commit
6d37e22c37
@ -74,7 +74,10 @@ export class Battle {
|
|||||||
public battleSeed: string = randomString(16, true);
|
public battleSeed: string = randomString(16, true);
|
||||||
private battleSeedState: string | null = null;
|
private battleSeedState: string | null = null;
|
||||||
public moneyScattered = 0;
|
public moneyScattered = 0;
|
||||||
/** Primarily for double battles, keeps track of last enemy and player pokemon that triggered its ability or used a move */
|
/**
|
||||||
|
* Primarily for double battles, keeps track of last enemy and player pokemon that triggered its ability or used a move
|
||||||
|
* @todo THis is only used for Sticky Web and in a really jank way...
|
||||||
|
*/
|
||||||
public lastEnemyInvolved: number;
|
public lastEnemyInvolved: number;
|
||||||
public lastPlayerInvolved: number;
|
public lastPlayerInvolved: number;
|
||||||
public lastUsedPokeball: PokeballType | null = null;
|
public lastUsedPokeball: PokeballType | null = null;
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
/* biome-ignore-start lint/correctness/noUnusedImports: tsdoc imports */
|
/* biome-ignore-start lint/correctness/noUnusedImports: tsdoc imports */
|
||||||
import type { BattleScene } from "#app/battle-scene";
|
import type { BattleScene } from "#app/battle-scene";
|
||||||
import type { SpeciesFormChangeRevertWeatherFormTrigger } from "#data/form-change-triggers";
|
import type { SpeciesFormChangeRevertWeatherFormTrigger } from "#data/form-change-triggers";
|
||||||
|
import type { MoveEffectPhase } from "#phases/move-effect-phase";
|
||||||
|
import type { MoveReflectPhase } from "#phases/move-reflect-phase";
|
||||||
/* biome-ignore-end lint/correctness/noUnusedImports: tsdoc imports */
|
/* biome-ignore-end lint/correctness/noUnusedImports: tsdoc imports */
|
||||||
|
|
||||||
import { applyAbAttrs } from "#abilities/apply-ab-attrs";
|
import { applyAbAttrs } from "#abilities/apply-ab-attrs";
|
||||||
@ -52,7 +54,8 @@ import { BerryModifierType } from "#modifiers/modifier-type";
|
|||||||
import { applyMoveAttrs } from "#moves/apply-attrs";
|
import { applyMoveAttrs } from "#moves/apply-attrs";
|
||||||
import { noAbilityTypeOverrideMoves } from "#moves/invalid-moves";
|
import { noAbilityTypeOverrideMoves } from "#moves/invalid-moves";
|
||||||
import type { Move } from "#moves/move";
|
import type { Move } from "#moves/move";
|
||||||
import type { PokemonMove } from "#moves/pokemon-move";
|
import { getMoveTargets } from "#moves/move-utils";
|
||||||
|
import { PokemonMove } from "#moves/pokemon-move";
|
||||||
import type { StatStageChangePhase } from "#phases/stat-stage-change-phase";
|
import type { StatStageChangePhase } from "#phases/stat-stage-change-phase";
|
||||||
import type {
|
import type {
|
||||||
AbAttrCondition,
|
AbAttrCondition,
|
||||||
@ -5748,13 +5751,24 @@ export class InfiltratorAbAttr extends AbAttr {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Attribute implementing the effects of {@link https://bulbapedia.bulbagarden.net/wiki/Magic_Bounce_(ability) | Magic Bounce}.
|
* Attribute implementing the effects of {@link https://bulbapedia.bulbagarden.net/wiki/Magic_Bounce_(ability) | Magic Bounce}.
|
||||||
|
*
|
||||||
* Allows the source to bounce back {@linkcode MoveFlags.REFLECTABLE | Reflectable}
|
* Allows the source to bounce back {@linkcode MoveFlags.REFLECTABLE | Reflectable}
|
||||||
* moves as if the user had used {@linkcode MoveId.MAGIC_COAT | Magic Coat}.
|
* moves as if the user had used {@linkcode MoveId.MAGIC_COAT | Magic Coat}.
|
||||||
* @sealed
|
*
|
||||||
* @todo Make reflection a part of this ability's effects
|
* The calling {@linkcode MoveEffectPhase} will "skip" targets with a reflection effect active,
|
||||||
|
* showing the flyout and queueing the reaction during the pending {@linkcode MoveReflectPhase}.
|
||||||
*/
|
*/
|
||||||
export class ReflectStatusMoveAbAttr extends AbAttr {
|
export class ReflectStatusMoveAbAttr extends PreDefendAbAttr {
|
||||||
private declare readonly _: never;
|
override apply({ pokemon, opponent, move }: AugmentMoveInteractionAbAttrParams): void {
|
||||||
|
const newTargets = move.isMultiTarget() ? getMoveTargets(pokemon, move.id).targets : [opponent.getBattlerIndex()];
|
||||||
|
globalScene.phaseManager.unshiftNew(
|
||||||
|
"MovePhase",
|
||||||
|
pokemon,
|
||||||
|
newTargets,
|
||||||
|
new PokemonMove(move.id),
|
||||||
|
MoveUseMode.REFLECTED,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Make these ability attributes be flags instead of dummy attributes
|
// TODO: Make these ability attributes be flags instead of dummy attributes
|
||||||
@ -7200,10 +7214,7 @@ export function initAbilities() {
|
|||||||
.attr(PostIntimidateStatStageChangeAbAttr, [ Stat.SPD ], 1),
|
.attr(PostIntimidateStatStageChangeAbAttr, [ Stat.SPD ], 1),
|
||||||
new Ability(AbilityId.MAGIC_BOUNCE, 5)
|
new Ability(AbilityId.MAGIC_BOUNCE, 5)
|
||||||
.attr(ReflectStatusMoveAbAttr)
|
.attr(ReflectStatusMoveAbAttr)
|
||||||
.ignorable()
|
.ignorable(),
|
||||||
// Interactions with stomping tantrum, instruct, encore, and probably other moves that
|
|
||||||
// rely on move history
|
|
||||||
.edgeCase(),
|
|
||||||
new Ability(AbilityId.SAP_SIPPER, 5)
|
new Ability(AbilityId.SAP_SIPPER, 5)
|
||||||
.attr(TypeImmunityStatStageChangeAbAttr, PokemonType.GRASS, Stat.ATK, 1)
|
.attr(TypeImmunityStatStageChangeAbAttr, PokemonType.GRASS, Stat.ATK, 1)
|
||||||
.ignorable(),
|
.ignorable(),
|
||||||
@ -7237,7 +7248,7 @@ export function initAbilities() {
|
|||||||
.attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonTeravolt", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }))
|
.attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonTeravolt", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }))
|
||||||
.attr(MoveAbilityBypassAbAttr),
|
.attr(MoveAbilityBypassAbAttr),
|
||||||
new Ability(AbilityId.AROMA_VEIL, 6)
|
new Ability(AbilityId.AROMA_VEIL, 6)
|
||||||
.attr(UserFieldBattlerTagImmunityAbAttr, [ BattlerTagType.INFATUATED, BattlerTagType.TAUNT, BattlerTagType.DISABLED, BattlerTagType.TORMENT, BattlerTagType.HEAL_BLOCK ])
|
.attr(UserFieldBattlerTagImmunityAbAttr, [ BattlerTagType.INFATUATED, BattlerTagType.TAUNT, BattlerTagType.DISABLED, BattlerTagType.TORMENT, BattlerTagType.HEAL_BLOCK, BattlerTagType.ENCORE ])
|
||||||
.ignorable(),
|
.ignorable(),
|
||||||
new Ability(AbilityId.FLOWER_VEIL, 6)
|
new Ability(AbilityId.FLOWER_VEIL, 6)
|
||||||
.attr(ConditionalUserFieldStatusEffectImmunityAbAttr, (target: Pokemon, source: Pokemon | null) => {
|
.attr(ConditionalUserFieldStatusEffectImmunityAbAttr, (target: Pokemon, source: Pokemon | null) => {
|
||||||
@ -7301,7 +7312,7 @@ export function initAbilities() {
|
|||||||
new Ability(AbilityId.GOOEY, 6)
|
new Ability(AbilityId.GOOEY, 6)
|
||||||
.attr(PostDefendStatStageChangeAbAttr, (_target, _user, move) => move.hasFlag(MoveFlags.MAKES_CONTACT), Stat.SPD, -1, false),
|
.attr(PostDefendStatStageChangeAbAttr, (_target, _user, move) => move.hasFlag(MoveFlags.MAKES_CONTACT), Stat.SPD, -1, false),
|
||||||
new Ability(AbilityId.AERILATE, 6)
|
new Ability(AbilityId.AERILATE, 6)
|
||||||
.attr(MoveTypeChangeAbAttr, PokemonType.FLYING, 1.2, (_user, _target, move) => move.type === PokemonType.NORMAL),
|
.attr(MoveTypeChangeAbAttr, PokemonType.FLYING, 1.2, (_user, _target, move) => move.type === PokemonType.NORMAL),
|
||||||
new Ability(AbilityId.PARENTAL_BOND, 6)
|
new Ability(AbilityId.PARENTAL_BOND, 6)
|
||||||
.attr(AddSecondStrikeAbAttr, 0.25),
|
.attr(AddSecondStrikeAbAttr, 0.25),
|
||||||
new Ability(AbilityId.DARK_AURA, 6)
|
new Ability(AbilityId.DARK_AURA, 6)
|
||||||
|
@ -56,6 +56,7 @@ import { MoveCategory } from "#enums/move-category";
|
|||||||
import { MoveFlags } from "#enums/move-flags";
|
import { MoveFlags } from "#enums/move-flags";
|
||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
import { MoveResult } from "#enums/move-result";
|
import { MoveResult } from "#enums/move-result";
|
||||||
|
import { MoveTarget } from "#enums/move-target";
|
||||||
import { MoveUseMode } from "#enums/move-use-mode";
|
import { MoveUseMode } from "#enums/move-use-mode";
|
||||||
import { PokemonAnimType } from "#enums/pokemon-anim-type";
|
import { PokemonAnimType } from "#enums/pokemon-anim-type";
|
||||||
import { PokemonType } from "#enums/pokemon-type";
|
import { PokemonType } from "#enums/pokemon-type";
|
||||||
@ -67,6 +68,8 @@ import type { Pokemon } from "#field/pokemon";
|
|||||||
import { applyMoveAttrs } from "#moves/apply-attrs";
|
import { applyMoveAttrs } from "#moves/apply-attrs";
|
||||||
import { invalidEncoreMoves } from "#moves/invalid-moves";
|
import { invalidEncoreMoves } from "#moves/invalid-moves";
|
||||||
import type { Move } from "#moves/move";
|
import type { Move } from "#moves/move";
|
||||||
|
import { getMoveTargets } from "#moves/move-utils";
|
||||||
|
import { PokemonMove } from "#moves/pokemon-move";
|
||||||
import type { MoveEffectPhase } from "#phases/move-effect-phase";
|
import type { MoveEffectPhase } from "#phases/move-effect-phase";
|
||||||
import type { MovePhase } from "#phases/move-phase";
|
import type { MovePhase } from "#phases/move-phase";
|
||||||
import type { StatStageChangeCallback } from "#phases/stat-stage-change-phase";
|
import type { StatStageChangeCallback } from "#phases/stat-stage-change-phase";
|
||||||
@ -175,6 +178,7 @@ export class BattlerTag implements BaseBattlerTag {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Make this a getter
|
||||||
isSourceLinked(): boolean {
|
isSourceLinked(): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -1229,13 +1233,16 @@ export class FrenzyTag extends SerializableBattlerTag {
|
|||||||
*/
|
*/
|
||||||
export class EncoreTag extends MoveRestrictionBattlerTag {
|
export class EncoreTag extends MoveRestrictionBattlerTag {
|
||||||
public override readonly tagType = BattlerTagType.ENCORE;
|
public override readonly tagType = BattlerTagType.ENCORE;
|
||||||
/** The ID of the move the user is locked into using */
|
/** The {@linkcode MoveId} the tag holder is locked into using. */
|
||||||
public moveId: MoveId;
|
public readonly moveId: MoveId;
|
||||||
|
|
||||||
constructor(sourceId: number) {
|
constructor(sourceId: number) {
|
||||||
|
// Encore ends at the end of the 3rd turn it procs.
|
||||||
|
// If used on turn X when faster, it ends at the end of turn X+2.
|
||||||
|
// If used on turn X when slower, it ends at the end of turn X+3.
|
||||||
super(
|
super(
|
||||||
BattlerTagType.ENCORE,
|
BattlerTagType.ENCORE,
|
||||||
[BattlerTagLapseType.CUSTOM, BattlerTagLapseType.AFTER_MOVE],
|
[BattlerTagLapseType.AFTER_MOVE, BattlerTagLapseType.TURN_END],
|
||||||
3,
|
3,
|
||||||
MoveId.ENCORE,
|
MoveId.ENCORE,
|
||||||
sourceId,
|
sourceId,
|
||||||
@ -1244,20 +1251,22 @@ export class EncoreTag extends MoveRestrictionBattlerTag {
|
|||||||
|
|
||||||
public override loadTag(source: BaseBattlerTag & Pick<EncoreTag, "tagType" | "moveId">): void {
|
public override loadTag(source: BaseBattlerTag & Pick<EncoreTag, "tagType" | "moveId">): void {
|
||||||
super.loadTag(source);
|
super.loadTag(source);
|
||||||
this.moveId = source.moveId;
|
(this as Mutable<this>).moveId = source.moveId;
|
||||||
}
|
}
|
||||||
|
|
||||||
override canAdd(pokemon: Pokemon): boolean {
|
override canAdd(pokemon: Pokemon): boolean {
|
||||||
const lastMove = pokemon.getLastNonVirtualMove();
|
const lastMove = pokemon.getLastNonVirtualMove();
|
||||||
if (!lastMove) {
|
if (
|
||||||
|
// No prior move has been used
|
||||||
|
!lastMove // Move is in the banlist
|
||||||
|
|| invalidEncoreMoves.has(lastMove.move) // Move is out of PP
|
||||||
|
|| !pokemon.getMoveset().some(m => m.moveId === lastMove.move && !m.isOutOfPp()) // Target has an active Shell Trap in waiting
|
||||||
|
|| pokemon.getTag(BattlerTagType.SHELL_TRAP)
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (invalidEncoreMoves.has(lastMove.move)) {
|
(this as Mutable<this>).moveId = lastMove.move;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.moveId = lastMove.move;
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -1269,31 +1278,51 @@ export class EncoreTag extends MoveRestrictionBattlerTag {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const movesetMove = pokemon.getMoveset().find(m => m.moveId === this.moveId);
|
// If the target has not moved yet,
|
||||||
if (movesetMove) {
|
// replace their upcoming move with the encored move against randomized targets
|
||||||
globalScene.phaseManager.changePhaseMove((phase: MovePhase) => phase.pokemon === pokemon, movesetMove);
|
const movePhase = globalScene.phaseManager.getMovePhase(m => m.pokemon === pokemon);
|
||||||
|
if (!movePhase) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use the prior move in the moveset.
|
||||||
|
// Bang is justified as `canAdd` returns `false` if not found
|
||||||
|
const movesetMove = pokemon.getMoveset().find(m => m.moveId === this.moveId && !m.isOutOfPp())!;
|
||||||
|
|
||||||
|
// TODO: Resolve this after the move failure PR to occur during the start of the MP -
|
||||||
|
// after sleep but before all usability checks
|
||||||
|
movePhase.move = movesetMove;
|
||||||
|
movePhase["targets"] = this.getTargets(pokemon);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTargets(pokemon: Pokemon): BattlerIndex[] {
|
||||||
|
// Edge case for Acupressure - always targets self
|
||||||
|
if (allMoves[this.moveId].moveTarget === MoveTarget.USER_OR_NEAR_ALLY) {
|
||||||
|
return [pokemon.getBattlerIndex()];
|
||||||
|
}
|
||||||
|
|
||||||
|
const moveTargets = getMoveTargets(pokemon, this.moveId);
|
||||||
|
// Spread moves and ones with only 1 valid target will use their normal targeting.
|
||||||
|
// If not, target a random enemy in our target list
|
||||||
|
return moveTargets.multiple || moveTargets.targets.length === 1
|
||||||
|
? moveTargets.targets
|
||||||
|
: [moveTargets.targets[pokemon.randBattleSeedInt(moveTargets.targets.length)]];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* If the encored move has run out of PP, Encore ends early. Otherwise, Encore lapses based on the AFTER_MOVE battler tag lapse type.
|
|
||||||
* @returns `true` to persist | `false` to end and be removed
|
|
||||||
*/
|
|
||||||
override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||||
if (lapseType === BattlerTagLapseType.CUSTOM) {
|
// If the encored move has run out of PP or the tag's turn count has elapsed,
|
||||||
const encoredMove = pokemon.getMoveset().find(m => m.moveId === this.moveId);
|
// Encore ends at the END of the turn.
|
||||||
return encoredMove != null && encoredMove.getPpRatio() > 0;
|
// Otherwise, Encore's duration reduces when the target attempts to use a move.
|
||||||
|
if (lapseType === BattlerTagLapseType.AFTER_MOVE) {
|
||||||
|
this.turnCount--;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return super.lapse(pokemon, lapseType);
|
|
||||||
|
const encoredMove = pokemon.getMoveset().find(m => m.moveId === this.moveId && !m.isOutOfPp());
|
||||||
|
return encoredMove && this.turnCount > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public override isMoveRestricted(move: MoveId): boolean {
|
||||||
* Checks if the move matches the moveId stored within the tag and returns a boolean value
|
|
||||||
* @param move - The ID of the move selected
|
|
||||||
* @param user N/A
|
|
||||||
* @returns `true` if the move does not match with the moveId stored and as a result, restricted
|
|
||||||
*/
|
|
||||||
override isMoveRestricted(move: MoveId, _user?: Pokemon): boolean {
|
|
||||||
return move !== this.moveId;
|
return move !== this.moveId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1487,6 +1516,7 @@ export class DrowsyTag extends SerializableBattlerTag {
|
|||||||
|
|
||||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||||
if (!super.lapse(pokemon, lapseType)) {
|
if (!super.lapse(pokemon, lapseType)) {
|
||||||
|
// TODO: Safeguard should not prevent yawn from setting sleep after tag use
|
||||||
pokemon.trySetStatus(StatusEffect.SLEEP);
|
pokemon.trySetStatus(StatusEffect.SLEEP);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -3620,6 +3650,23 @@ export class MagicCoatTag extends BattlerTag {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the tag to reflect a move.
|
||||||
|
* @param pokemon - The {@linkcode Pokemon} to whom this tag belongs
|
||||||
|
* @param opponent - The {@linkcode Pokemon} having originally used the move
|
||||||
|
* @param move - The {@linkcode Move} being used
|
||||||
|
*/
|
||||||
|
public apply(pokemon: Pokemon, opponent: Pokemon, move: Move): void {
|
||||||
|
const newTargets = move.isMultiTarget() ? getMoveTargets(pokemon, move.id).targets : [opponent.getBattlerIndex()];
|
||||||
|
globalScene.phaseManager.unshiftNew(
|
||||||
|
"MovePhase",
|
||||||
|
pokemon,
|
||||||
|
newTargets,
|
||||||
|
new PokemonMove(move.id),
|
||||||
|
MoveUseMode.REFLECTED,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -3702,7 +3749,7 @@ export function getBattlerTag(
|
|||||||
case BattlerTagType.AQUA_RING:
|
case BattlerTagType.AQUA_RING:
|
||||||
return new AquaRingTag();
|
return new AquaRingTag();
|
||||||
case BattlerTagType.DROWSY:
|
case BattlerTagType.DROWSY:
|
||||||
return new DrowsyTag();
|
return new DrowsyTag(sourceId);
|
||||||
case BattlerTagType.TRAPPED:
|
case BattlerTagType.TRAPPED:
|
||||||
return new TrappedTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);
|
return new TrappedTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);
|
||||||
case BattlerTagType.NO_RETREAT:
|
case BattlerTagType.NO_RETREAT:
|
||||||
|
@ -1,5 +1,20 @@
|
|||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array containing all move-calling moves, used for DRY when writing move banlists
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const moveCallingMoves = [
|
||||||
|
MoveId.ASSIST,
|
||||||
|
MoveId.COPYCAT,
|
||||||
|
MoveId.ME_FIRST,
|
||||||
|
MoveId.METRONOME,
|
||||||
|
MoveId.MIRROR_MOVE,
|
||||||
|
MoveId.NATURE_POWER,
|
||||||
|
MoveId.SLEEP_TALK,
|
||||||
|
MoveId.SNATCH,
|
||||||
|
] as const;
|
||||||
|
|
||||||
/** Set of moves that cannot be called by {@linkcode MoveId.METRONOME | Metronome}. */
|
/** Set of moves that cannot be called by {@linkcode MoveId.METRONOME | Metronome}. */
|
||||||
export const invalidMetronomeMoves: ReadonlySet<MoveId> = new Set([
|
export const invalidMetronomeMoves: ReadonlySet<MoveId> = new Set([
|
||||||
MoveId.AFTER_YOU,
|
MoveId.AFTER_YOU,
|
||||||
@ -272,11 +287,72 @@ export const invalidSketchMoves: ReadonlySet<MoveId> = new Set([
|
|||||||
|
|
||||||
/** Set of all moves that cannot be locked into by {@linkcode MoveId.ENCORE}. */
|
/** Set of all moves that cannot be locked into by {@linkcode MoveId.ENCORE}. */
|
||||||
export const invalidEncoreMoves: ReadonlySet<MoveId> = new Set([
|
export const invalidEncoreMoves: ReadonlySet<MoveId> = new Set([
|
||||||
MoveId.MIMIC,
|
...moveCallingMoves,
|
||||||
MoveId.MIRROR_MOVE,
|
|
||||||
MoveId.TRANSFORM,
|
MoveId.TRANSFORM,
|
||||||
MoveId.STRUGGLE,
|
MoveId.MIMIC,
|
||||||
MoveId.SKETCH,
|
MoveId.SKETCH,
|
||||||
|
MoveId.STRUGGLE,
|
||||||
|
MoveId.DYNAMAX_CANNON,
|
||||||
MoveId.SLEEP_TALK,
|
MoveId.SLEEP_TALK,
|
||||||
MoveId.ENCORE,
|
MoveId.ENCORE,
|
||||||
|
// NB: Add Max/G-Max/Z-Move blockage if or when they are implemented
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Set of all moves that cannot be repeated by {@linkcode MoveId.INSTRUCT}. */
|
||||||
|
export const invalidInstructMoves: ReadonlySet<MoveId> = new Set([
|
||||||
|
// Locking/Continually Executed moves
|
||||||
|
MoveId.OUTRAGE,
|
||||||
|
MoveId.RAGING_FURY,
|
||||||
|
MoveId.ROLLOUT,
|
||||||
|
MoveId.PETAL_DANCE,
|
||||||
|
MoveId.THRASH,
|
||||||
|
MoveId.ICE_BALL,
|
||||||
|
MoveId.UPROAR,
|
||||||
|
// Multi-turn Moves
|
||||||
|
MoveId.BIDE,
|
||||||
|
MoveId.SHELL_TRAP,
|
||||||
|
MoveId.BEAK_BLAST,
|
||||||
|
MoveId.FOCUS_PUNCH,
|
||||||
|
// "First Turn Only" moves
|
||||||
|
MoveId.FAKE_OUT,
|
||||||
|
MoveId.FIRST_IMPRESSION,
|
||||||
|
MoveId.MAT_BLOCK,
|
||||||
|
// Moves with a recharge turn
|
||||||
|
MoveId.HYPER_BEAM,
|
||||||
|
MoveId.ETERNABEAM,
|
||||||
|
MoveId.FRENZY_PLANT,
|
||||||
|
MoveId.BLAST_BURN,
|
||||||
|
MoveId.HYDRO_CANNON,
|
||||||
|
MoveId.GIGA_IMPACT,
|
||||||
|
MoveId.PRISMATIC_LASER,
|
||||||
|
MoveId.ROAR_OF_TIME,
|
||||||
|
MoveId.ROCK_WRECKER,
|
||||||
|
MoveId.METEOR_ASSAULT,
|
||||||
|
// Charging & 2-turn moves
|
||||||
|
MoveId.DIG,
|
||||||
|
MoveId.FLY,
|
||||||
|
MoveId.BOUNCE,
|
||||||
|
MoveId.SHADOW_FORCE,
|
||||||
|
MoveId.PHANTOM_FORCE,
|
||||||
|
MoveId.DIVE,
|
||||||
|
MoveId.ELECTRO_SHOT,
|
||||||
|
MoveId.ICE_BURN,
|
||||||
|
MoveId.GEOMANCY,
|
||||||
|
MoveId.FREEZE_SHOCK,
|
||||||
|
MoveId.SKY_DROP,
|
||||||
|
MoveId.SKY_ATTACK,
|
||||||
|
MoveId.SKULL_BASH,
|
||||||
|
MoveId.SOLAR_BEAM,
|
||||||
|
MoveId.SOLAR_BLADE,
|
||||||
|
MoveId.METEOR_BEAM,
|
||||||
|
// Copying/Move-Calling moves + Instruct
|
||||||
|
...moveCallingMoves,
|
||||||
|
MoveId.INSTRUCT,
|
||||||
|
// Misc moves
|
||||||
|
MoveId.KINGS_SHIELD,
|
||||||
|
MoveId.SKETCH,
|
||||||
|
MoveId.TRANSFORM,
|
||||||
|
MoveId.MIMIC,
|
||||||
|
MoveId.STRUGGLE,
|
||||||
|
// NB: Add Max/G-Max/Z-Move blockage if or when they are implemented
|
||||||
]);
|
]);
|
||||||
|
@ -78,7 +78,7 @@ import {
|
|||||||
PreserveBerryModifier,
|
PreserveBerryModifier,
|
||||||
} from "#modifiers/modifier";
|
} from "#modifiers/modifier";
|
||||||
import { applyMoveAttrs } from "#moves/apply-attrs";
|
import { applyMoveAttrs } from "#moves/apply-attrs";
|
||||||
import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSketchMoves, invalidSleepTalkMoves } from "#moves/invalid-moves";
|
import { invalidAssistMoves, invalidCopycatMoves, invalidInstructMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSketchMoves, invalidSleepTalkMoves } from "#moves/invalid-moves";
|
||||||
import { frenzyMissFunc, getMoveTargets } from "#moves/move-utils";
|
import { frenzyMissFunc, getMoveTargets } from "#moves/move-utils";
|
||||||
import { PokemonMove } from "#moves/pokemon-move";
|
import { PokemonMove } from "#moves/pokemon-move";
|
||||||
import { MovePhase } from "#phases/move-phase";
|
import { MovePhase } from "#phases/move-phase";
|
||||||
@ -680,20 +680,9 @@ export abstract class Move implements Localizable {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case MoveFlags.REFLECTABLE:
|
|
||||||
// If the target is not semi-invulnerable and either has magic coat active or an unignored magic bounce ability
|
|
||||||
if (
|
|
||||||
target?.getTag(SemiInvulnerableTag) ||
|
|
||||||
!(target?.getTag(BattlerTagType.MAGIC_COAT) ||
|
|
||||||
(!this.doesFlagEffectApply({ flag: MoveFlags.IGNORE_ABILITIES, user, target }) &&
|
|
||||||
target?.hasAbilityWithAttr("ReflectStatusMoveAbAttr")))
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return !!(this.flags & flag);
|
return this.hasFlag(flag)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -7095,10 +7084,11 @@ export class CopyMoveAttr extends CallMoveAttr {
|
|||||||
* Attribute used for moves that cause the target to repeat their last used move.
|
* Attribute used for moves that cause the target to repeat their last used move.
|
||||||
*
|
*
|
||||||
* Used by {@linkcode MoveId.INSTRUCT | Instruct}.
|
* Used by {@linkcode MoveId.INSTRUCT | Instruct}.
|
||||||
* @see [Instruct on Bulbapedia](https://bulbapedia.bulbagarden.net/wiki/Instruct_(move))
|
* @see {@link https://bulbapedia.bulbagarden.net/wiki/Instruct_(move) | Instruct on Bulbapedia}
|
||||||
*/
|
*/
|
||||||
export class RepeatMoveAttr extends MoveEffectAttr {
|
export class RepeatMoveAttr extends MoveEffectAttr {
|
||||||
private movesetMove: PokemonMove;
|
private movesetMove: PokemonMove;
|
||||||
|
private lastMoveTargets: BattlerIndex[];
|
||||||
constructor() {
|
constructor() {
|
||||||
super(false, { trigger: MoveEffectTrigger.POST_APPLY }); // needed to ensure correct protect interaction
|
super(false, { trigger: MoveEffectTrigger.POST_APPLY }); // needed to ensure correct protect interaction
|
||||||
}
|
}
|
||||||
@ -7110,17 +7100,14 @@ export class RepeatMoveAttr extends MoveEffectAttr {
|
|||||||
* @returns `true` if the move succeeds
|
* @returns `true` if the move succeeds
|
||||||
*/
|
*/
|
||||||
apply(user: Pokemon, target: Pokemon): boolean {
|
apply(user: Pokemon, target: Pokemon): boolean {
|
||||||
// get the last move used (excluding status based failures) as well as the corresponding moveset slot
|
|
||||||
// bangs are justified as Instruct fails if no prior move or moveset move exists
|
|
||||||
// TODO: How does instruct work when copying a move called via Copycat that the user itself knows?
|
|
||||||
const lastMove = target.getLastNonVirtualMove()!;
|
|
||||||
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove?.move)!
|
|
||||||
|
|
||||||
// If the last move used can hit more than one target or has variable targets,
|
// If the last move used can hit more than one target or has variable targets,
|
||||||
// re-compute the targets for the attack (mainly for alternating double/single battles)
|
// re-compute the targets for the attack (mainly for alternating double/single battles)
|
||||||
// Rampaging moves (e.g. Outrage) are not included due to being incompatible with Instruct,
|
// Rampaging moves (e.g. Outrage) are not included due to being incompatible with Instruct,
|
||||||
// nor is Dragon Darts (due to its smart targeting bypassing normal target selection)
|
// nor is Dragon Darts (due to its smart targeting bypassing normal target selection)
|
||||||
let moveTargets = this.movesetMove.getMove().isMultiTarget() ? getMoveTargets(target, this.movesetMove.moveId).targets : lastMove.targets;
|
// TODO: Revisit this once target computation is moved to mid-use
|
||||||
|
let moveTargets = this.movesetMove.getMove().isMultiTarget()
|
||||||
|
? getMoveTargets(target, this.movesetMove.moveId).targets
|
||||||
|
: this.lastMoveTargets;
|
||||||
|
|
||||||
// In the event the instructed move's only target is a fainted opponent, redirect it to an alive ally if possible.
|
// In the event the instructed move's only target is a fainted opponent, redirect it to an alive ally if possible.
|
||||||
// Normally, all yet-unexecuted move phases would swap targets after any foe faints or flees (see `redirectPokemonMoves` in `battle-scene.ts`),
|
// Normally, all yet-unexecuted move phases would swap targets after any foe faints or flees (see `redirectPokemonMoves` in `battle-scene.ts`),
|
||||||
@ -7138,6 +7125,13 @@ export class RepeatMoveAttr extends MoveEffectAttr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the target is currently affected by Encore, increase its duration by 1 (to offset decrease during move use)
|
||||||
|
// TODO: There might be a better way of doing this...
|
||||||
|
const targetEncore = target.getTag(BattlerTagType.ENCORE) as EncoreTag | undefined;
|
||||||
|
if (targetEncore) {
|
||||||
|
targetEncore["turnCount"]++
|
||||||
|
}
|
||||||
|
|
||||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:instructingMove", {
|
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:instructingMove", {
|
||||||
userPokemonName: getPokemonNameWithAffix(user),
|
userPokemonName: getPokemonNameWithAffix(user),
|
||||||
targetPokemonName: getPokemonNameWithAffix(target)
|
targetPokemonName: getPokemonNameWithAffix(target)
|
||||||
@ -7150,82 +7144,20 @@ export class RepeatMoveAttr extends MoveEffectAttr {
|
|||||||
getCondition(): MoveConditionFunc {
|
getCondition(): MoveConditionFunc {
|
||||||
return (_user, target, _move) => {
|
return (_user, target, _move) => {
|
||||||
// TODO: Check instruct behavior with struggle - ignore, fail or success
|
// TODO: Check instruct behavior with struggle - ignore, fail or success
|
||||||
|
// TODO: How does instruct work when copying a move called via Copycat that the user itself knows?
|
||||||
const lastMove = target.getLastNonVirtualMove();
|
const lastMove = target.getLastNonVirtualMove();
|
||||||
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove?.move);
|
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove?.move);
|
||||||
const uninstructableMoves = [
|
|
||||||
// Locking/Continually Executed moves
|
|
||||||
MoveId.OUTRAGE,
|
|
||||||
MoveId.RAGING_FURY,
|
|
||||||
MoveId.ROLLOUT,
|
|
||||||
MoveId.PETAL_DANCE,
|
|
||||||
MoveId.THRASH,
|
|
||||||
MoveId.ICE_BALL,
|
|
||||||
MoveId.UPROAR,
|
|
||||||
// Multi-turn Moves
|
|
||||||
MoveId.BIDE,
|
|
||||||
MoveId.SHELL_TRAP,
|
|
||||||
MoveId.BEAK_BLAST,
|
|
||||||
MoveId.FOCUS_PUNCH,
|
|
||||||
// "First Turn Only" moves
|
|
||||||
MoveId.FAKE_OUT,
|
|
||||||
MoveId.FIRST_IMPRESSION,
|
|
||||||
MoveId.MAT_BLOCK,
|
|
||||||
// Moves with a recharge turn
|
|
||||||
MoveId.HYPER_BEAM,
|
|
||||||
MoveId.ETERNABEAM,
|
|
||||||
MoveId.FRENZY_PLANT,
|
|
||||||
MoveId.BLAST_BURN,
|
|
||||||
MoveId.HYDRO_CANNON,
|
|
||||||
MoveId.GIGA_IMPACT,
|
|
||||||
MoveId.PRISMATIC_LASER,
|
|
||||||
MoveId.ROAR_OF_TIME,
|
|
||||||
MoveId.ROCK_WRECKER,
|
|
||||||
MoveId.METEOR_ASSAULT,
|
|
||||||
// Charging & 2-turn moves
|
|
||||||
MoveId.DIG,
|
|
||||||
MoveId.FLY,
|
|
||||||
MoveId.BOUNCE,
|
|
||||||
MoveId.SHADOW_FORCE,
|
|
||||||
MoveId.PHANTOM_FORCE,
|
|
||||||
MoveId.DIVE,
|
|
||||||
MoveId.ELECTRO_SHOT,
|
|
||||||
MoveId.ICE_BURN,
|
|
||||||
MoveId.GEOMANCY,
|
|
||||||
MoveId.FREEZE_SHOCK,
|
|
||||||
MoveId.SKY_DROP,
|
|
||||||
MoveId.SKY_ATTACK,
|
|
||||||
MoveId.SKULL_BASH,
|
|
||||||
MoveId.SOLAR_BEAM,
|
|
||||||
MoveId.SOLAR_BLADE,
|
|
||||||
MoveId.METEOR_BEAM,
|
|
||||||
// Copying/Move-Calling moves
|
|
||||||
MoveId.ASSIST,
|
|
||||||
MoveId.COPYCAT,
|
|
||||||
MoveId.ME_FIRST,
|
|
||||||
MoveId.METRONOME,
|
|
||||||
MoveId.MIRROR_MOVE,
|
|
||||||
MoveId.NATURE_POWER,
|
|
||||||
MoveId.SLEEP_TALK,
|
|
||||||
MoveId.SNATCH,
|
|
||||||
MoveId.INSTRUCT,
|
|
||||||
// Misc moves
|
|
||||||
MoveId.KINGS_SHIELD,
|
|
||||||
MoveId.SKETCH,
|
|
||||||
MoveId.TRANSFORM,
|
|
||||||
MoveId.MIMIC,
|
|
||||||
MoveId.STRUGGLE,
|
|
||||||
// TODO: Add Max/G-Max/Z-Move blockage if or when they are implemented
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!lastMove?.move // no move to instruct
|
if (
|
||||||
|
!lastMove?.move // no move to instruct
|
||||||
|| !movesetMove // called move not in target's moveset (forgetting the move, etc.)
|
|| !movesetMove // called move not in target's moveset (forgetting the move, etc.)
|
||||||
|| movesetMove.ppUsed === movesetMove.getMovePp() // move out of pp
|
|| movesetMove.isOutOfPp() // move out of pp
|
||||||
// TODO: This next line is likely redundant as all charging moves are in the above list
|
|| invalidInstructMoves.has(lastMove.move) // called move is in the banlist
|
||||||
|| allMoves[lastMove.move].isChargingMove() // called move is a charging/recharging move
|
) {
|
||||||
|| uninstructableMoves.includes(lastMove.move)) { // called move is in the banlist
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this.movesetMove = movesetMove;
|
this.movesetMove = movesetMove;
|
||||||
|
this.lastMoveTargets = lastMove.targets
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -9091,11 +9023,11 @@ export function initMoves() {
|
|||||||
.hidesUser(),
|
.hidesUser(),
|
||||||
new StatusMove(MoveId.ENCORE, PokemonType.NORMAL, 100, 5, -1, 0, 2)
|
new StatusMove(MoveId.ENCORE, PokemonType.NORMAL, 100, 5, -1, 0, 2)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.ENCORE, false, true)
|
.attr(AddBattlerTagAttr, BattlerTagType.ENCORE, false, true)
|
||||||
.ignoresSubstitute()
|
|
||||||
.condition((user, target, move) => new EncoreTag(user.id).canAdd(target))
|
.condition((user, target, move) => new EncoreTag(user.id).canAdd(target))
|
||||||
|
.ignoresSubstitute()
|
||||||
.reflectable()
|
.reflectable()
|
||||||
// Can lock infinitely into struggle; has incorrect interactions with Blood Moon/Gigaton Hammer
|
// has incorrect interactions with Blood Moon/Gigaton Hammer
|
||||||
// Also may or may not incorrectly select targets for replacement move (needs verification)
|
// TODO: Verify if Encore's duration decreases during status based move failures
|
||||||
.edgeCase(),
|
.edgeCase(),
|
||||||
new AttackMove(MoveId.PURSUIT, PokemonType.DARK, MoveCategory.PHYSICAL, 40, 100, 20, -1, 0, 2)
|
new AttackMove(MoveId.PURSUIT, PokemonType.DARK, MoveCategory.PHYSICAL, 40, 100, 20, -1, 0, 2)
|
||||||
.partial(), // No effect implemented
|
.partial(), // No effect implemented
|
||||||
@ -9272,9 +9204,7 @@ export function initMoves() {
|
|||||||
new SelfStatusMove(MoveId.MAGIC_COAT, PokemonType.PSYCHIC, -1, 15, -1, 4, 3)
|
new SelfStatusMove(MoveId.MAGIC_COAT, PokemonType.PSYCHIC, -1, 15, -1, 4, 3)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.MAGIC_COAT, true, true, 0)
|
.attr(AddBattlerTagAttr, BattlerTagType.MAGIC_COAT, true, true, 0)
|
||||||
.condition(failIfLastCondition)
|
.condition(failIfLastCondition)
|
||||||
// Interactions with stomping tantrum, instruct, and other moves that
|
// Should reflect moves that would otherwise fail
|
||||||
// rely on move history
|
|
||||||
// Also will not reflect roar / whirlwind if the target has ForceSwitchOutImmunityAbAttr
|
|
||||||
.edgeCase(),
|
.edgeCase(),
|
||||||
new SelfStatusMove(MoveId.RECYCLE, PokemonType.NORMAL, -1, 10, -1, 0, 3)
|
new SelfStatusMove(MoveId.RECYCLE, PokemonType.NORMAL, -1, 10, -1, 0, 3)
|
||||||
.unimplemented(),
|
.unimplemented(),
|
||||||
@ -9285,9 +9215,11 @@ export function initMoves() {
|
|||||||
new StatusMove(MoveId.YAWN, PokemonType.NORMAL, -1, 10, -1, 0, 3)
|
new StatusMove(MoveId.YAWN, PokemonType.NORMAL, -1, 10, -1, 0, 3)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.DROWSY, false, true)
|
.attr(AddBattlerTagAttr, BattlerTagType.DROWSY, false, true)
|
||||||
.condition((user, target, move) => !target.status && !target.isSafeguarded(user))
|
.condition((user, target, move) => !target.status && !target.isSafeguarded(user))
|
||||||
.reflectable(),
|
.reflectable()
|
||||||
new AttackMove(MoveId.KNOCK_OFF, PokemonType.DARK, MoveCategory.PHYSICAL, 65, 100, 20, -1, 0, 3)
|
// Does not count as failed for terrain-based failures; should not check Safeguard when triggering drowsiness
|
||||||
.attr(MovePowerMultiplierAttr, (user, target, move) => target.getHeldItems().filter(i => i.isTransferable).length > 0 ? 1.5 : 1)
|
.edgeCase(),
|
||||||
|
new AttackMove(MoveId.KNOCK_OFF, PokemonType.DARK, MoveCategory.PHYSICAL, 65, 100, 20, -1, 0, 3)
|
||||||
|
.attr(MovePowerMultiplierAttr, (user, target, move) => target.getHeldItems().some(i => i.isTransferable) ? 1.5 : 1)
|
||||||
.attr(RemoveHeldItemAttr, false)
|
.attr(RemoveHeldItemAttr, false)
|
||||||
.edgeCase(),
|
.edgeCase(),
|
||||||
// Should not be able to remove held item if user faints due to Rough Skin, Iron Barbs, etc.
|
// Should not be able to remove held item if user faints due to Rough Skin, Iron Barbs, etc.
|
||||||
@ -10565,11 +10497,10 @@ export function initMoves() {
|
|||||||
new AttackMove(MoveId.TROP_KICK, PokemonType.GRASS, MoveCategory.PHYSICAL, 70, 100, 15, 100, 0, 7)
|
new AttackMove(MoveId.TROP_KICK, PokemonType.GRASS, MoveCategory.PHYSICAL, 70, 100, 15, 100, 0, 7)
|
||||||
.attr(StatStageChangeAttr, [ Stat.ATK ], -1),
|
.attr(StatStageChangeAttr, [ Stat.ATK ], -1),
|
||||||
new StatusMove(MoveId.INSTRUCT, PokemonType.PSYCHIC, -1, 15, -1, 0, 7)
|
new StatusMove(MoveId.INSTRUCT, PokemonType.PSYCHIC, -1, 15, -1, 0, 7)
|
||||||
.ignoresSubstitute()
|
|
||||||
.attr(RepeatMoveAttr)
|
.attr(RepeatMoveAttr)
|
||||||
|
.ignoresSubstitute()
|
||||||
/*
|
/*
|
||||||
* Incorrect interactions with Gigaton Hammer, Blood Moon & Torment due to them _failing on use_, not merely being unselectable.
|
* Incorrect interactions with Gigaton Hammer, Blood Moon & Torment due to them _failing on use_, not merely being unselectable.
|
||||||
* Incorrectly ticks down Encore's fail counter
|
|
||||||
* TODO: Verify whether Instruct can repeat Struggle
|
* TODO: Verify whether Instruct can repeat Struggle
|
||||||
* TODO: Verify whether Instruct can fail when using a copied move also in one's own moveset
|
* TODO: Verify whether Instruct can fail when using a copied move also in one's own moveset
|
||||||
*/
|
*/
|
||||||
|
@ -72,6 +72,7 @@ export class PokemonMove {
|
|||||||
this.ppUsed = Math.min(this.ppUsed + count, this.getMovePp());
|
this.ppUsed = Math.min(this.ppUsed + count, this.getMovePp());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Rename to `getMaxPP`
|
||||||
getMovePp(): number {
|
getMovePp(): number {
|
||||||
return this.maxPpOverride || this.getMove().pp + this.ppUp * toDmgValue(this.getMove().pp / 5);
|
return this.maxPpOverride || this.getMove().pp + this.ppUp * toDmgValue(this.getMove().pp / 5);
|
||||||
}
|
}
|
||||||
@ -80,6 +81,10 @@ export class PokemonMove {
|
|||||||
return 1 - this.ppUsed / this.getMovePp();
|
return 1 - this.ppUsed / this.getMovePp();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isOutOfPp(): boolean {
|
||||||
|
return this.ppUsed >= this.getMovePp();
|
||||||
|
}
|
||||||
|
|
||||||
getName(): string {
|
getName(): string {
|
||||||
return this.getMove().name;
|
return this.getMove().name;
|
||||||
}
|
}
|
||||||
|
@ -3532,6 +3532,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
* Calculates the damage of an attack made by another Pokemon against this Pokemon
|
* Calculates the damage of an attack made by another Pokemon against this Pokemon
|
||||||
* @param __namedParameters.source - Needed for proper typedoc rendering
|
* @param __namedParameters.source - Needed for proper typedoc rendering
|
||||||
* @returns The {@linkcode DamageCalculationResult}
|
* @returns The {@linkcode DamageCalculationResult}
|
||||||
|
* @todo Condense various multipliers into a single function
|
||||||
*/
|
*/
|
||||||
getAttackDamage({
|
getAttackDamage({
|
||||||
source,
|
source,
|
||||||
@ -4089,6 +4090,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
: this.summonData.tags.find(t => t.tagType === tagType);
|
: this.summonData.tags.find(t => t.tagType === tagType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findTag<T extends BattlerTag>(tagFilter: (tag: BattlerTag) => tag is T): T | undefined;
|
||||||
|
findTag(tagFilter: (tag: BattlerTag) => boolean): BattlerTag | undefined;
|
||||||
/**
|
/**
|
||||||
* Find the first `BattlerTag` matching the specified predicate
|
* Find the first `BattlerTag` matching the specified predicate
|
||||||
* @remarks
|
* @remarks
|
||||||
@ -4299,8 +4302,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Return this Pokemon's move history.
|
* Return this Pokemon's move history.
|
||||||
* Entries are sorted in order of OLDEST to NEWEST
|
* Entries are sorted in order of OLDEST to NEWEST.
|
||||||
* @returns An array of {@linkcode TurnMove}, as described above.
|
* @returns An array of {@linkcode TurnMove}s, as described above.
|
||||||
* @see {@linkcode getLastXMoves}
|
* @see {@linkcode getLastXMoves}
|
||||||
*/
|
*/
|
||||||
public getMoveHistory(): TurnMove[] {
|
public getMoveHistory(): TurnMove[] {
|
||||||
@ -4308,9 +4311,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a new entry to this Pokemon's move history
|
* Add a move to the end of this {@linkcode Pokemon}'s move history,
|
||||||
* @remarks
|
* used to record its most recently executed actions.
|
||||||
* Does nothing if this Pokemon is not currently on the field.
|
|
||||||
* @param turnMove - The move to add to the history
|
* @param turnMove - The move to add to the history
|
||||||
*/
|
*/
|
||||||
public pushMoveHistory(turnMove: TurnMove): void {
|
public pushMoveHistory(turnMove: TurnMove): void {
|
||||||
|
@ -57,6 +57,7 @@ import { MoveEffectPhase } from "#phases/move-effect-phase";
|
|||||||
import { MoveEndPhase } from "#phases/move-end-phase";
|
import { MoveEndPhase } from "#phases/move-end-phase";
|
||||||
import { MoveHeaderPhase } from "#phases/move-header-phase";
|
import { MoveHeaderPhase } from "#phases/move-header-phase";
|
||||||
import { MovePhase } from "#phases/move-phase";
|
import { MovePhase } from "#phases/move-phase";
|
||||||
|
import { MoveReflectPhase } from "#phases/move-reflect-phase";
|
||||||
import {
|
import {
|
||||||
MysteryEncounterBattlePhase,
|
MysteryEncounterBattlePhase,
|
||||||
MysteryEncounterBattleStartCleanupPhase,
|
MysteryEncounterBattleStartCleanupPhase,
|
||||||
@ -163,6 +164,7 @@ const PHASES = Object.freeze({
|
|||||||
MoveEffectPhase,
|
MoveEffectPhase,
|
||||||
MoveEndPhase,
|
MoveEndPhase,
|
||||||
MoveHeaderPhase,
|
MoveHeaderPhase,
|
||||||
|
MoveReflectPhase,
|
||||||
MovePhase,
|
MovePhase,
|
||||||
MysteryEncounterPhase,
|
MysteryEncounterPhase,
|
||||||
MysteryEncounterOptionSelectedPhase,
|
MysteryEncounterOptionSelectedPhase,
|
||||||
|
@ -179,11 +179,6 @@ export class CommandPhase extends FieldPhase {
|
|||||||
|
|
||||||
this.checkCommander();
|
this.checkCommander();
|
||||||
|
|
||||||
const playerPokemon = this.getPokemon();
|
|
||||||
|
|
||||||
// Note: It is OK to call this if the target is not under the effect of encore; it will simply do nothing.
|
|
||||||
playerPokemon.lapseTag(BattlerTagType.ENCORE);
|
|
||||||
|
|
||||||
if (globalScene.currentBattle.turnCommands[this.fieldIndex]?.skip) {
|
if (globalScene.currentBattle.turnCommands[this.fieldIndex]?.skip) {
|
||||||
this.end();
|
this.end();
|
||||||
return;
|
return;
|
||||||
|
@ -33,8 +33,7 @@ import {
|
|||||||
} from "#modifiers/modifier";
|
} from "#modifiers/modifier";
|
||||||
import { applyFilteredMoveAttrs, applyMoveAttrs } from "#moves/apply-attrs";
|
import { applyFilteredMoveAttrs, applyMoveAttrs } from "#moves/apply-attrs";
|
||||||
import type { Move, MoveAttr } from "#moves/move";
|
import type { Move, MoveAttr } from "#moves/move";
|
||||||
import { getMoveTargets, isFieldTargeted } from "#moves/move-utils";
|
import { isFieldTargeted } from "#moves/move-utils";
|
||||||
import { PokemonMove } from "#moves/pokemon-move";
|
|
||||||
import { PokemonPhase } from "#phases/pokemon-phase";
|
import { PokemonPhase } from "#phases/pokemon-phase";
|
||||||
import { DamageAchv } from "#system/achv";
|
import { DamageAchv } from "#system/achv";
|
||||||
import type { DamageResult } from "#types/damage-result";
|
import type { DamageResult } from "#types/damage-result";
|
||||||
@ -45,6 +44,18 @@ import i18next from "i18next";
|
|||||||
|
|
||||||
export type HitCheckEntry = [HitCheckResult, TypeDamageMultiplier];
|
export type HitCheckEntry = [HitCheckResult, TypeDamageMultiplier];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type representing the resolved of a move's damage processing.
|
||||||
|
*/
|
||||||
|
type MoveDamageTuple = [
|
||||||
|
/** The {@linkcode HitResult} of the interaction. */
|
||||||
|
result: HitResult,
|
||||||
|
/** The final amount of damage that was dealt. */
|
||||||
|
damage: number,
|
||||||
|
/** Whether the attack was a critical hit. */
|
||||||
|
wasCritical: boolean,
|
||||||
|
];
|
||||||
|
|
||||||
export class MoveEffectPhase extends PokemonPhase {
|
export class MoveEffectPhase extends PokemonPhase {
|
||||||
public readonly phaseName = "MoveEffectPhase";
|
public readonly phaseName = "MoveEffectPhase";
|
||||||
public move: Move;
|
public move: Move;
|
||||||
@ -89,140 +100,11 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
this.hitChecks = new Array(this.targets.length).fill([HitCheckResult.PENDING, 0]);
|
this.hitChecks = new Array(this.targets.length).fill([HitCheckResult.PENDING, 0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Compute targets and the results of hit checks of the invoked move against all targets,
|
|
||||||
* organized by battler index.
|
|
||||||
*
|
|
||||||
* **This is *not* a pure function**; it has the following side effects
|
|
||||||
* - `this.hitChecks` - The results of the hit checks against each target
|
|
||||||
* - `this.moveHistoryEntry` - Sets success or failure based on the hit check results
|
|
||||||
* - user.turnData.hitCount and user.turnData.hitsLeft - Both set to 1 if the
|
|
||||||
* move was unsuccessful against all targets
|
|
||||||
*
|
|
||||||
* @returns The targets of the invoked move
|
|
||||||
* @see {@linkcode hitCheck}
|
|
||||||
*/
|
|
||||||
private conductHitChecks(user: Pokemon, fieldMove: boolean): Pokemon[] {
|
|
||||||
/** All Pokemon targeted by this phase's invoked move */
|
|
||||||
/** Whether any hit check ended in a success */
|
|
||||||
let anySuccess = false;
|
|
||||||
/** Whether the attack missed all of its targets */
|
|
||||||
let allMiss = true;
|
|
||||||
|
|
||||||
let targets = this.getTargets();
|
|
||||||
|
|
||||||
// For field targeted moves, we only look for the first target that may magic bounce
|
|
||||||
|
|
||||||
for (const [i, target] of targets.entries()) {
|
|
||||||
const hitCheck = this.hitCheck(target);
|
|
||||||
// If the move bounced and was a field targeted move,
|
|
||||||
// then immediately stop processing other targets
|
|
||||||
if (fieldMove && hitCheck[0] === HitCheckResult.REFLECTED) {
|
|
||||||
targets = [target];
|
|
||||||
this.hitChecks = [hitCheck];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (hitCheck[0] === HitCheckResult.HIT) {
|
|
||||||
anySuccess = true;
|
|
||||||
} else {
|
|
||||||
allMiss ||= hitCheck[0] === HitCheckResult.MISS;
|
|
||||||
}
|
|
||||||
this.hitChecks[i] = hitCheck;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (anySuccess) {
|
|
||||||
this.moveHistoryEntry.result = MoveResult.SUCCESS;
|
|
||||||
} else {
|
|
||||||
user.turnData.hitCount = 1;
|
|
||||||
user.turnData.hitsLeft = 1;
|
|
||||||
this.moveHistoryEntry.result = allMiss ? MoveResult.MISS : MoveResult.FAIL;
|
|
||||||
}
|
|
||||||
|
|
||||||
return targets;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Queue the phases that should occur when the target reflects the move back to the user
|
|
||||||
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
|
||||||
* @param target - The {@linkcode Pokemon} that is reflecting the move
|
|
||||||
* TODO: Rework this to use `onApply` of Magic Coat
|
|
||||||
*/
|
|
||||||
private queueReflectedMove(user: Pokemon, target: Pokemon): void {
|
|
||||||
const newTargets = this.move.isMultiTarget()
|
|
||||||
? getMoveTargets(target, this.move.id).targets
|
|
||||||
: [user.getBattlerIndex()];
|
|
||||||
// TODO: ability displays should be handled by the ability
|
|
||||||
if (!target.getTag(BattlerTagType.MAGIC_COAT)) {
|
|
||||||
globalScene.phaseManager.unshiftNew(
|
|
||||||
"ShowAbilityPhase",
|
|
||||||
target.getBattlerIndex(),
|
|
||||||
target.getPassiveAbility().hasAttr("ReflectStatusMoveAbAttr"),
|
|
||||||
);
|
|
||||||
globalScene.phaseManager.unshiftNew("HideAbilityPhase");
|
|
||||||
}
|
|
||||||
|
|
||||||
globalScene.phaseManager.unshiftNew(
|
|
||||||
"MovePhase",
|
|
||||||
target,
|
|
||||||
newTargets,
|
|
||||||
new PokemonMove(this.move.id),
|
|
||||||
MoveUseMode.REFLECTED,
|
|
||||||
MovePhaseTimingModifier.FIRST,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply the move to each of the resolved targets.
|
|
||||||
* @param targets - The resolved set of targets of the move
|
|
||||||
* @throws Error if there was an unexpected hit check result
|
|
||||||
*/
|
|
||||||
private applyToTargets(user: Pokemon, targets: Pokemon[]): void {
|
|
||||||
let firstHit = true;
|
|
||||||
for (const [i, target] of targets.entries()) {
|
|
||||||
const [hitCheckResult, effectiveness] = this.hitChecks[i];
|
|
||||||
switch (hitCheckResult) {
|
|
||||||
case HitCheckResult.HIT:
|
|
||||||
this.applyMoveEffects(target, effectiveness, firstHit);
|
|
||||||
firstHit = false;
|
|
||||||
if (isFieldTargeted(this.move)) {
|
|
||||||
// Stop processing other targets if the move is a field move
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
// biome-ignore lint/suspicious/noFallthroughSwitchClause: The fallthrough is intentional
|
|
||||||
case HitCheckResult.NO_EFFECT:
|
|
||||||
globalScene.phaseManager.queueMessage(
|
|
||||||
i18next.t(this.move.id === MoveId.SHEER_COLD ? "battle:hitResultImmune" : "battle:hitResultNoEffect", {
|
|
||||||
pokemonName: getPokemonNameWithAffix(target),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
case HitCheckResult.NO_EFFECT_NO_MESSAGE:
|
|
||||||
case HitCheckResult.PROTECTED:
|
|
||||||
case HitCheckResult.TARGET_NOT_ON_FIELD:
|
|
||||||
applyMoveAttrs("NoEffectAttr", user, target, this.move);
|
|
||||||
break;
|
|
||||||
case HitCheckResult.MISS:
|
|
||||||
globalScene.phaseManager.queueMessage(
|
|
||||||
i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(target) }),
|
|
||||||
);
|
|
||||||
applyMoveAttrs("MissEffectAttr", user, target, this.move);
|
|
||||||
break;
|
|
||||||
case HitCheckResult.REFLECTED:
|
|
||||||
this.queueReflectedMove(user, target);
|
|
||||||
break;
|
|
||||||
case HitCheckResult.PENDING:
|
|
||||||
case HitCheckResult.ERROR:
|
|
||||||
throw new Error("Unexpected hit check result");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override start(): void {
|
public override start(): void {
|
||||||
super.start();
|
super.start();
|
||||||
|
|
||||||
/** The Pokemon using this phase's invoked move */
|
/** The Pokemon using this phase's invoked move */
|
||||||
const user = this.getUserPokemon();
|
const user = this.getUserPokemon();
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
super.end();
|
super.end();
|
||||||
return;
|
return;
|
||||||
@ -317,6 +199,58 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
this.postAnimCallback(user, targets);
|
this.postAnimCallback(user, targets);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute targets and the results of hit checks of the invoked move against all targets,
|
||||||
|
* organized by battler index.
|
||||||
|
*
|
||||||
|
* **This is *not* a pure function**; it has the following side effects
|
||||||
|
* - `this.hitChecks` - The results of the hit checks against each target
|
||||||
|
* - `this.moveHistoryEntry` - Sets success or failure based on the hit check results
|
||||||
|
* - user.turnData.hitCount and user.turnData.hitsLeft - Both set to 1 if the
|
||||||
|
* move was unsuccessful against all targets
|
||||||
|
*
|
||||||
|
* @returns The targets of the invoked move
|
||||||
|
* @see {@linkcode hitCheck}
|
||||||
|
*/
|
||||||
|
private conductHitChecks(user: Pokemon, fieldMove: boolean): Pokemon[] {
|
||||||
|
/** All Pokemon targeted by this phase's invoked move */
|
||||||
|
/** Whether any hit check ended in a success */
|
||||||
|
let anySuccess = false;
|
||||||
|
/** Whether the attack missed all of its targets */
|
||||||
|
let allMiss = true;
|
||||||
|
|
||||||
|
let targets = this.getTargets();
|
||||||
|
|
||||||
|
// For field targeted moves, we only look for the first target that may magic bounce
|
||||||
|
|
||||||
|
for (const [i, target] of targets.entries()) {
|
||||||
|
const hitCheck = this.hitCheck(target);
|
||||||
|
// If the move bounced and was a field targeted move,
|
||||||
|
// then immediately stop processing other targets
|
||||||
|
if (fieldMove && hitCheck[0] === HitCheckResult.REFLECTED) {
|
||||||
|
targets = [target];
|
||||||
|
this.hitChecks = [hitCheck];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (hitCheck[0] === HitCheckResult.HIT) {
|
||||||
|
anySuccess = true;
|
||||||
|
} else {
|
||||||
|
allMiss ||= hitCheck[0] === HitCheckResult.MISS;
|
||||||
|
}
|
||||||
|
this.hitChecks[i] = hitCheck;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anySuccess) {
|
||||||
|
this.moveHistoryEntry.result = MoveResult.SUCCESS;
|
||||||
|
} else {
|
||||||
|
user.turnData.hitCount = 1;
|
||||||
|
user.turnData.hitsLeft = 1;
|
||||||
|
this.moveHistoryEntry.result = allMiss ? MoveResult.MISS : MoveResult.FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return targets;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback to be called after the move animation is played
|
* Callback to be called after the move animation is played
|
||||||
*/
|
*/
|
||||||
@ -348,128 +282,52 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
this.end();
|
this.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override end(): void {
|
|
||||||
const user = this.getUserPokemon();
|
|
||||||
if (!user) {
|
|
||||||
super.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If this phase isn't for the invoked move's last strike (and we still have something to hit),
|
|
||||||
* unshift another MoveEffectPhase for the next strike before ending this phase.
|
|
||||||
*/
|
|
||||||
if (--user.turnData.hitsLeft >= 1 && this.getFirstTarget()) {
|
|
||||||
this.addNextHitPhase();
|
|
||||||
super.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* All hits of the move have resolved by now.
|
|
||||||
* Queue message for multi-strike moves before applying Shell Bell heals & proccing Dancer-like effects.
|
|
||||||
*/
|
|
||||||
const hitsTotal = user.turnData.hitCount - Math.max(user.turnData.hitsLeft, 0);
|
|
||||||
if (hitsTotal > 1 || user.turnData.hitsLeft > 0) {
|
|
||||||
// Queue message if multiple hits occurred or were slated to occur (such as a Triple Axel miss)
|
|
||||||
globalScene.phaseManager.queueMessage(i18next.t("battle:attackHitsCount", { count: hitsTotal }));
|
|
||||||
}
|
|
||||||
|
|
||||||
globalScene.applyModifiers(HitHealModifier, this.player, user);
|
|
||||||
this.getTargets().forEach(target => {
|
|
||||||
target.turnData.moveEffectiveness = null;
|
|
||||||
});
|
|
||||||
super.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies reactive effects that occur when a Pokémon is hit.
|
* Apply the move to each of the resolved targets.
|
||||||
* (i.e. Effect Spore, Disguise, Liquid Ooze, Beak Blast)
|
* @param targets - The resolved set of targets of the move
|
||||||
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
* @throws Error if there was an unexpected hit check result
|
||||||
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
|
|
||||||
* @param hitResult - The {@linkcode HitResult} of the attempted move
|
|
||||||
* @param damage - The amount of damage dealt to the target in the interaction
|
|
||||||
* @param wasCritical - `true` if the move was a critical hit
|
|
||||||
*/
|
*/
|
||||||
protected applyOnGetHitAbEffects(
|
private applyToTargets(user: Pokemon, targets: Pokemon[]): void {
|
||||||
user: Pokemon,
|
let firstHit = true;
|
||||||
target: Pokemon,
|
for (const [i, target] of targets.entries()) {
|
||||||
hitResult: HitResult,
|
const [hitCheckResult, effectiveness] = this.hitChecks[i];
|
||||||
damage: number,
|
switch (hitCheckResult) {
|
||||||
wasCritical = false,
|
case HitCheckResult.HIT:
|
||||||
): void {
|
this.applyMoveEffects(target, effectiveness, firstHit);
|
||||||
const params = { pokemon: target, opponent: user, move: this.move, hitResult, damage };
|
firstHit = false;
|
||||||
applyAbAttrs("PostDefendAbAttr", params);
|
if (isFieldTargeted(this.move)) {
|
||||||
|
// Stop processing other targets if the move is a field move
|
||||||
if (wasCritical) {
|
return;
|
||||||
applyAbAttrs("PostReceiveCritStatStageChangeAbAttr", params);
|
}
|
||||||
}
|
break;
|
||||||
target.lapseTags(BattlerTagLapseType.AFTER_HIT);
|
// biome-ignore lint/suspicious/noFallthroughSwitchClause: The fallthrough is intentional
|
||||||
}
|
case HitCheckResult.NO_EFFECT:
|
||||||
|
globalScene.phaseManager.queueMessage(
|
||||||
/**
|
i18next.t(this.move.id === MoveId.SHEER_COLD ? "battle:hitResultImmune" : "battle:hitResultNoEffect", {
|
||||||
* Handles checking for and applying Flinches
|
pokemonName: getPokemonNameWithAffix(target),
|
||||||
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
}),
|
||||||
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
|
);
|
||||||
* @param dealsDamage - `true` if the attempted move successfully dealt damage
|
case HitCheckResult.NO_EFFECT_NO_MESSAGE:
|
||||||
*/
|
case HitCheckResult.PROTECTED:
|
||||||
protected applyHeldItemFlinchCheck(user: Pokemon, target: Pokemon, dealsDamage: boolean): void {
|
case HitCheckResult.TARGET_NOT_ON_FIELD:
|
||||||
if (this.move.hasAttr("FlinchAttr")) {
|
applyMoveAttrs("NoEffectAttr", user, target, this.move);
|
||||||
return;
|
break;
|
||||||
}
|
case HitCheckResult.MISS:
|
||||||
|
globalScene.phaseManager.queueMessage(
|
||||||
if (
|
i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(target) }),
|
||||||
dealsDamage
|
);
|
||||||
&& !target.hasAbilityWithAttr("IgnoreMoveEffectsAbAttr")
|
applyMoveAttrs("MissEffectAttr", user, target, this.move);
|
||||||
&& !this.move.hitsSubstitute(user, target)
|
break;
|
||||||
) {
|
case HitCheckResult.REFLECTED:
|
||||||
const flinched = new BooleanHolder(false);
|
globalScene.phaseManager.unshiftNew("MoveReflectPhase", target, user, this.move);
|
||||||
globalScene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched);
|
break;
|
||||||
if (flinched.value) {
|
case HitCheckResult.PENDING:
|
||||||
target.addTag(BattlerTagType.FLINCHED, undefined, this.move.id, user.id);
|
case HitCheckResult.ERROR:
|
||||||
|
throw new Error("Unexpected hit check result");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Return whether the target is protected by protect or a relevant conditional protection
|
|
||||||
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
|
||||||
* @param target - {@linkcode Pokemon} the target to check for protection
|
|
||||||
* @param move - The {@linkcode Move} being used
|
|
||||||
* @returns Whether the pokemon was protected
|
|
||||||
*/
|
|
||||||
private protectedCheck(user: Pokemon, target: Pokemon): boolean {
|
|
||||||
/** The {@linkcode ArenaTagSide} to which the target belongs */
|
|
||||||
const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
|
||||||
/** Has the invoked move been cancelled by conditional protection (e.g Quick Guard)? */
|
|
||||||
const hasConditionalProtectApplied = new BooleanHolder(false);
|
|
||||||
/** Does the applied conditional protection bypass Protect-ignoring effects? */
|
|
||||||
const bypassIgnoreProtect = new BooleanHolder(false);
|
|
||||||
/** If the move is not targeting a Pokemon on the user's side, try to apply conditional protection effects */
|
|
||||||
if (!this.move.isAllyTarget()) {
|
|
||||||
globalScene.arena.applyTagsForSide(
|
|
||||||
ConditionalProtectTag,
|
|
||||||
targetSide,
|
|
||||||
false,
|
|
||||||
hasConditionalProtectApplied,
|
|
||||||
user,
|
|
||||||
target,
|
|
||||||
this.move.id,
|
|
||||||
bypassIgnoreProtect,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Break up this chunky boolean to make it more palatable
|
|
||||||
return (
|
|
||||||
![MoveTarget.ENEMY_SIDE, MoveTarget.BOTH_SIDES].includes(this.move.moveTarget)
|
|
||||||
&& (bypassIgnoreProtect.value || !this.move.doesFlagEffectApply({ flag: MoveFlags.IGNORE_PROTECT, user, target }))
|
|
||||||
&& (hasConditionalProtectApplied.value
|
|
||||||
|| (target.findTags(t => t instanceof DamageProtectedTag).length === 0
|
|
||||||
&& target.findTags(t => t instanceof ProtectedTag).some(t => target.lapseTag(t.tagType)))
|
|
||||||
|| (this.move.category !== MoveCategory.STATUS
|
|
||||||
&& target.findTags(t => t instanceof DamageProtectedTag).some(t => target.lapseTag(t.tagType))))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Conduct the hit check and type effectiveness for this move against the target
|
* Conduct the hit check and type effectiveness for this move against the target
|
||||||
*
|
*
|
||||||
@ -490,10 +348,6 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
const user = this.getUserPokemon();
|
const user = this.getUserPokemon();
|
||||||
const move = this.move;
|
const move = this.move;
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return [HitCheckResult.ERROR, 0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Moves targeting the user bypass all checks
|
// Moves targeting the user bypass all checks
|
||||||
if (move.moveTarget === MoveTarget.USER) {
|
if (move.moveTarget === MoveTarget.USER) {
|
||||||
return [HitCheckResult.HIT, 1];
|
return [HitCheckResult.HIT, 1];
|
||||||
@ -527,7 +381,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reflected moves cannot be reflected again
|
// Reflected moves cannot be reflected again
|
||||||
if (!isReflected(this.useMode) && move.doesFlagEffectApply({ flag: MoveFlags.REFLECTABLE, user, target })) {
|
if (isMoveReflectableBy(this.move, target, this.useMode)) {
|
||||||
return [HitCheckResult.REFLECTED, 0];
|
return [HitCheckResult.REFLECTED, 0];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -599,9 +453,6 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
*/
|
*/
|
||||||
public checkBypassAccAndInvuln(target: Pokemon) {
|
public checkBypassAccAndInvuln(target: Pokemon) {
|
||||||
const user = this.getUserPokemon();
|
const user = this.getUserPokemon();
|
||||||
if (!user) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (user.hasAbilityWithAttr("AlwaysHitAbAttr") || target.hasAbilityWithAttr("AlwaysHitAbAttr")) {
|
if (user.hasAbilityWithAttr("AlwaysHitAbAttr") || target.hasAbilityWithAttr("AlwaysHitAbAttr")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -634,83 +485,42 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @todo Investigate why this doesn't use `BattlerIndex`
|
* Check whether the target is protected by protect or a relevant conditional protection.
|
||||||
* @returns The {@linkcode Pokemon} using this phase's invoked move
|
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
||||||
|
* @param target - The target {@linkcode Pokemon} to check for protection
|
||||||
|
* @returns Whether the target was protected
|
||||||
*/
|
*/
|
||||||
public getUserPokemon(): Pokemon | undefined {
|
private protectedCheck(user: Pokemon, target: Pokemon): boolean {
|
||||||
// TODO: Make this purely a battler index
|
/** The {@linkcode ArenaTagSide} to which the target belongs */
|
||||||
if (this.battlerIndex > BattlerIndex.ENEMY_2) {
|
const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||||
return globalScene.getPokemonById(this.battlerIndex);
|
/** Has the invoked move been cancelled by conditional protection (e.g Quick Guard)? */
|
||||||
|
const hasConditionalProtectApplied = new BooleanHolder(false);
|
||||||
|
/** Does the applied conditional protection bypass Protect-ignoring effects? */
|
||||||
|
const bypassIgnoreProtect = new BooleanHolder(false);
|
||||||
|
/** If the move is not targeting a Pokemon on the user's side, try to apply conditional protection effects */
|
||||||
|
if (!this.move.isAllyTarget()) {
|
||||||
|
globalScene.arena.applyTagsForSide(
|
||||||
|
ConditionalProtectTag,
|
||||||
|
targetSide,
|
||||||
|
false,
|
||||||
|
hasConditionalProtectApplied,
|
||||||
|
user,
|
||||||
|
target,
|
||||||
|
this.move.id,
|
||||||
|
bypassIgnoreProtect,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// TODO: Figure out why this uses `fieldIndex` instead of `BattlerIndex`
|
|
||||||
// TODO: Remove `?? undefined` once field pokemon getters are made sane
|
|
||||||
return (this.player ? globalScene.getPlayerField() : globalScene.getEnemyField())[this.fieldIndex] ?? undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// TODO: Break up this chunky boolean to make it more palatable
|
||||||
* @returns An array of {@linkcode Pokemon} that are:
|
return (
|
||||||
* - On-field and active
|
![MoveTarget.ENEMY_SIDE, MoveTarget.BOTH_SIDES].includes(this.move.moveTarget)
|
||||||
* - Non-fainted
|
&& (bypassIgnoreProtect.value || !this.move.doesFlagEffectApply({ flag: MoveFlags.IGNORE_PROTECT, user, target }))
|
||||||
* - Targeted by this phase's invoked move
|
&& (hasConditionalProtectApplied.value
|
||||||
*/
|
|| (target.findTags(t => t instanceof DamageProtectedTag).length === 0
|
||||||
public getTargets(): Pokemon[] {
|
&& target.findTags(t => t instanceof ProtectedTag).some(t => target.lapseTag(t.tagType)))
|
||||||
return globalScene.getField(true).filter(p => this.targets.indexOf(p.getBattlerIndex()) > -1);
|
|| (this.move.category !== MoveCategory.STATUS
|
||||||
}
|
&& target.findTags(t => t instanceof DamageProtectedTag).some(t => target.lapseTag(t.tagType))))
|
||||||
|
);
|
||||||
/** @returns The first active, non-fainted target of this phase's invoked move. */
|
|
||||||
public getFirstTarget(): Pokemon | undefined {
|
|
||||||
return this.getTargets()[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the given {@linkcode Pokemon} from this phase's target list
|
|
||||||
* @param target - The {@linkcode Pokemon} to be removed
|
|
||||||
*/
|
|
||||||
protected removeTarget(target: Pokemon): void {
|
|
||||||
const targetIndex = this.targets.indexOf(target.getBattlerIndex());
|
|
||||||
if (targetIndex !== -1) {
|
|
||||||
this.targets.splice(this.targets.indexOf(target.getBattlerIndex()), 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prevents subsequent strikes of this phase's invoked move from occurring
|
|
||||||
* @param target - If defined, only stop subsequent strikes against this {@linkcode Pokemon}
|
|
||||||
*/
|
|
||||||
public stopMultiHit(target?: Pokemon): void {
|
|
||||||
// If given a specific target, remove the target from subsequent strikes
|
|
||||||
if (target) {
|
|
||||||
this.removeTarget(target);
|
|
||||||
}
|
|
||||||
const user = this.getUserPokemon();
|
|
||||||
if (!user) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// If no target specified, or the specified target was the last of this move's
|
|
||||||
// targets, completely cancel all subsequent strikes.
|
|
||||||
if (!target || this.targets.length === 0) {
|
|
||||||
user.turnData.hitCount = 1;
|
|
||||||
user.turnData.hitsLeft = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unshifts a new `MoveEffectPhase` with the same properties as this phase.
|
|
||||||
* Used to queue the next hit of multi-strike moves.
|
|
||||||
*/
|
|
||||||
protected addNextHitPhase(): void {
|
|
||||||
globalScene.phaseManager.unshiftNew("MoveEffectPhase", this.battlerIndex, this.targets, this.move, this.useMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Removes all substitutes that were broken by this phase's invoked move */
|
|
||||||
protected updateSubstitutes(): void {
|
|
||||||
const targets = this.getTargets();
|
|
||||||
for (const target of targets) {
|
|
||||||
const substitute = target.getTag(SubstituteTag);
|
|
||||||
if (substitute && substitute.hp <= 0) {
|
|
||||||
target.lapseTag(BattlerTagType.SUBSTITUTE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -758,18 +568,15 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
*/
|
*/
|
||||||
protected applyMoveEffects(target: Pokemon, effectiveness: TypeDamageMultiplier, firstTarget: boolean): void {
|
protected applyMoveEffects(target: Pokemon, effectiveness: TypeDamageMultiplier, firstTarget: boolean): void {
|
||||||
const user = this.getUserPokemon();
|
const user = this.getUserPokemon();
|
||||||
if (user == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.triggerMoveEffects(MoveEffectTrigger.PRE_APPLY, user, target);
|
this.triggerMoveEffects(MoveEffectTrigger.PRE_APPLY, user, target);
|
||||||
|
|
||||||
const [hitResult, wasCritical, dmg] = this.applyMove(user, target, effectiveness);
|
const result = this.applyMove(user, target, effectiveness);
|
||||||
|
|
||||||
// Apply effects to the user (always) and the target (if not blocked by substitute).
|
// Apply effects to the user (always) and the target (if not blocked by substitute).
|
||||||
this.triggerMoveEffects(MoveEffectTrigger.POST_APPLY, user, target, firstTarget, true);
|
this.triggerMoveEffects(MoveEffectTrigger.POST_APPLY, user, target, firstTarget, true);
|
||||||
if (!this.move.hitsSubstitute(user, target)) {
|
if (!this.move.hitsSubstitute(user, target)) {
|
||||||
this.applyOnTargetEffects(user, target, hitResult, firstTarget, dmg, wasCritical);
|
this.applyOnTargetEffects(user, target, firstTarget, result);
|
||||||
}
|
}
|
||||||
if (this.lastHit) {
|
if (this.lastHit) {
|
||||||
globalScene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger);
|
globalScene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger);
|
||||||
@ -784,18 +591,43 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sub-method of for {@linkcode applyMoveEffects} that applies damage to the target.
|
* Apply the result of this phase's move to the given target.
|
||||||
*
|
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
||||||
|
* @param target - The {@linkcode Pokemon} struck by the move
|
||||||
|
* @param effectiveness - The effectiveness of the move against the target
|
||||||
|
* @returns A {@linkcode MoveDamageTuple} containing the results of damage application.
|
||||||
|
*/
|
||||||
|
protected applyMove(user: Pokemon, target: Pokemon, effectiveness: TypeDamageMultiplier): MoveDamageTuple {
|
||||||
|
const moveCategory = user.getMoveCategory(target, this.move);
|
||||||
|
|
||||||
|
if (moveCategory === MoveCategory.STATUS) {
|
||||||
|
return [HitResult.STATUS, 0, false];
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = this.applyMoveDamage(user, target, effectiveness);
|
||||||
|
|
||||||
|
if (user.turnData.hitsLeft === 1 || target.isFainted()) {
|
||||||
|
this.queueHitResultMessage(result[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.isFainted()) {
|
||||||
|
this.onFaintTarget(user, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sub-method of {@linkcode applyMove} that applies damage to the target.
|
||||||
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
||||||
* @param target - The {@linkcode Pokemon} targeted by the move
|
* @param target - The {@linkcode Pokemon} targeted by the move
|
||||||
* @param effectiveness - The effectiveness of the move against the target
|
* @param effectiveness - The type effectiveness of the move against the target
|
||||||
* @returns The {@linkcode HitResult} of the move against the target, a boolean indicating whether the target was crit, and the amount of damage dealt
|
* @returns A tuple containing:
|
||||||
|
* 1. The {@linkcode HitResult} of the move against the target
|
||||||
|
* 2. The final amount of damage dealt
|
||||||
|
* 3. Whether the attack was a critical hit
|
||||||
*/
|
*/
|
||||||
protected applyMoveDamage(
|
protected applyMoveDamage(user: Pokemon, target: Pokemon, effectiveness: TypeDamageMultiplier): MoveDamageTuple {
|
||||||
user: Pokemon,
|
|
||||||
target: Pokemon,
|
|
||||||
effectiveness: TypeDamageMultiplier,
|
|
||||||
): [result: HitResult, critical: boolean, damage: number] {
|
|
||||||
const isCritical = target.getCriticalHitResult(user, this.move);
|
const isCritical = target.getCriticalHitResult(user, this.move);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -816,19 +648,19 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
isCritical,
|
isCritical,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: Verify if flash fire/charge are consumed if damage is prevented
|
||||||
const typeBoost = user.findTag(
|
const typeBoost = user.findTag(
|
||||||
t => t instanceof TypeBoostTag && t.boostedType === user.getMoveType(this.move),
|
(t): t is TypeBoostTag => t instanceof TypeBoostTag && t.boostedType === user.getMoveType(this.move),
|
||||||
) as TypeBoostTag;
|
);
|
||||||
if (typeBoost?.oneUse) {
|
if (typeBoost?.oneUse) {
|
||||||
user.removeTag(typeBoost.tagType);
|
user.removeTag(typeBoost.tagType);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isOneHitKo = result === HitResult.ONE_HIT_KO;
|
if (dmg <= 0) {
|
||||||
|
return [result, 0, false];
|
||||||
if (!dmg) {
|
|
||||||
return [result, false, 0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isOneHitKo = result === HitResult.ONE_HIT_KO;
|
||||||
target.lapseTags(BattlerTagLapseType.HIT);
|
target.lapseTags(BattlerTagLapseType.HIT);
|
||||||
|
|
||||||
const substitute = target.getTag(SubstituteTag);
|
const substitute = target.getTag(SubstituteTag);
|
||||||
@ -855,7 +687,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (damage <= 0) {
|
if (damage <= 0) {
|
||||||
return [result, isCritical, damage];
|
return [result, 0, isCritical];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.isPlayer()) {
|
if (user.isPlayer()) {
|
||||||
@ -884,7 +716,30 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
globalScene.applyModifiers(DamageMoneyRewardModifier, true, user, new NumberHolder(damage));
|
globalScene.applyModifiers(DamageMoneyRewardModifier, true, user, new NumberHolder(damage));
|
||||||
}
|
}
|
||||||
|
|
||||||
return [result, isCritical, damage];
|
return [result, damage, isCritical];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sub-method of {@linkcode applyMove} that queues the hit-result message
|
||||||
|
* on the final strike of the move against a target
|
||||||
|
* @param result - The {@linkcode HitResult} of the move
|
||||||
|
*/
|
||||||
|
protected queueHitResultMessage(result: HitResult) {
|
||||||
|
let msg: string | undefined;
|
||||||
|
switch (result) {
|
||||||
|
case HitResult.SUPER_EFFECTIVE:
|
||||||
|
msg = i18next.t("battle:hitResultSuperEffective");
|
||||||
|
break;
|
||||||
|
case HitResult.NOT_VERY_EFFECTIVE:
|
||||||
|
msg = i18next.t("battle:hitResultNotVeryEffective");
|
||||||
|
break;
|
||||||
|
case HitResult.ONE_HIT_KO:
|
||||||
|
msg = i18next.t("battle:hitResultOneHitKO");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (msg) {
|
||||||
|
globalScene.phaseManager.queueMessage(msg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -908,76 +763,15 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sub-method of {@linkcode applyMove} that queues the hit-result message
|
* Sub-method of {@linkcode applyMovetEffects} that applies all effects aimed at the move's target.
|
||||||
* on the final strike of the move against a target
|
|
||||||
* @param result - The {@linkcode HitResult} of the move
|
|
||||||
*/
|
|
||||||
protected queueHitResultMessage(result: HitResult) {
|
|
||||||
let msg: string | undefined;
|
|
||||||
switch (result) {
|
|
||||||
case HitResult.SUPER_EFFECTIVE:
|
|
||||||
msg = i18next.t("battle:hitResultSuperEffective");
|
|
||||||
break;
|
|
||||||
case HitResult.NOT_VERY_EFFECTIVE:
|
|
||||||
msg = i18next.t("battle:hitResultNotVeryEffective");
|
|
||||||
break;
|
|
||||||
case HitResult.ONE_HIT_KO:
|
|
||||||
msg = i18next.t("battle:hitResultOneHitKo");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (msg) {
|
|
||||||
globalScene.phaseManager.queueMessage(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Apply the result of this phase's move to the given target
|
|
||||||
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
|
||||||
* @param target - The {@linkcode Pokemon} struck by the move
|
|
||||||
* @param effectiveness - The effectiveness of the move against the target
|
|
||||||
* @returns The {@linkcode HitResult} of the move against the target, a boolean indicating whether the target was crit, and the amount of damage dealt
|
|
||||||
*/
|
|
||||||
protected applyMove(
|
|
||||||
user: Pokemon,
|
|
||||||
target: Pokemon,
|
|
||||||
effectiveness: TypeDamageMultiplier,
|
|
||||||
): [HitResult, critical: boolean, damage: number] {
|
|
||||||
const moveCategory = user.getMoveCategory(target, this.move);
|
|
||||||
|
|
||||||
if (moveCategory === MoveCategory.STATUS) {
|
|
||||||
return [HitResult.STATUS, false, 0];
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = this.applyMoveDamage(user, target, effectiveness);
|
|
||||||
|
|
||||||
if (user.turnData.hitsLeft === 1 || target.isFainted()) {
|
|
||||||
this.queueHitResultMessage(result[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target.isFainted()) {
|
|
||||||
this.onFaintTarget(user, target);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies all effects aimed at the move's target.
|
|
||||||
* To be used when the target is successfully and directly hit by the move.
|
* To be used when the target is successfully and directly hit by the move.
|
||||||
* @param user - The {@linkcode Pokemon} using the move
|
* @param user - The {@linkcode Pokemon} using the move
|
||||||
* @param target - The {@linkcode Pokemon} targeted by the move
|
* @param target - The {@linkcode Pokemon} targeted by the move
|
||||||
* @param hitResult - The {@linkcode HitResult} obtained from applying the move
|
|
||||||
* @param firstTarget - `true` if the target is the first Pokemon hit by the attack
|
* @param firstTarget - `true` if the target is the first Pokemon hit by the attack
|
||||||
* @param damage - The amount of damage dealt to the target in the interaction
|
* @param tuple - A {@linkcode MoveDamageTuple} containing the resolved damage result.
|
||||||
* @param wasCritical - `true` if the move was a critical hit
|
|
||||||
*/
|
*/
|
||||||
protected applyOnTargetEffects(
|
protected applyOnTargetEffects(user: Pokemon, target: Pokemon, firstTarget: boolean, tuple: MoveDamageTuple): void {
|
||||||
user: Pokemon,
|
const [hitResult, damage] = tuple;
|
||||||
target: Pokemon,
|
|
||||||
hitResult: HitResult,
|
|
||||||
firstTarget: boolean,
|
|
||||||
damage: number,
|
|
||||||
wasCritical = false,
|
|
||||||
): void {
|
|
||||||
/** Does {@linkcode hitResult} indicate that damage was dealt to the target? */
|
/** Does {@linkcode hitResult} indicate that damage was dealt to the target? */
|
||||||
const dealsDamage = [
|
const dealsDamage = [
|
||||||
HitResult.EFFECTIVE,
|
HitResult.EFFECTIVE,
|
||||||
@ -988,7 +782,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
|
|
||||||
this.triggerMoveEffects(MoveEffectTrigger.POST_APPLY, user, target, firstTarget, false);
|
this.triggerMoveEffects(MoveEffectTrigger.POST_APPLY, user, target, firstTarget, false);
|
||||||
this.applyHeldItemFlinchCheck(user, target, dealsDamage);
|
this.applyHeldItemFlinchCheck(user, target, dealsDamage);
|
||||||
this.applyOnGetHitAbEffects(user, target, hitResult, damage, wasCritical);
|
this.applyOnGetHitAbEffects(user, target, tuple);
|
||||||
applyAbAttrs("PostAttackAbAttr", { pokemon: user, opponent: target, move: this.move, hitResult, damage });
|
applyAbAttrs("PostAttackAbAttr", { pokemon: user, opponent: target, move: this.move, hitResult, damage });
|
||||||
|
|
||||||
// We assume only enemy Pokemon are able to have the EnemyAttackStatusEffectChanceModifier from tokens
|
// We assume only enemy Pokemon are able to have the EnemyAttackStatusEffectChanceModifier from tokens
|
||||||
@ -1001,4 +795,176 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
globalScene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target);
|
globalScene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sub-method of {@linkcode applyOnTargetEffects} that applies reactive effects that occur when a Pokémon is hit.
|
||||||
|
* (i.e. Effect Spore, Disguise, Liquid Ooze, Beak Blast)
|
||||||
|
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
||||||
|
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
|
||||||
|
* @param hitResult - The {@linkcode HitResult} of the attempted move
|
||||||
|
* @param damage - The amount of damage dealt by the attack
|
||||||
|
* @param wasCritical - `true` if the move was a critical hit
|
||||||
|
*/
|
||||||
|
protected applyOnGetHitAbEffects(
|
||||||
|
user: Pokemon,
|
||||||
|
target: Pokemon,
|
||||||
|
[hitResult, damage, wasCritical]: MoveDamageTuple,
|
||||||
|
): void {
|
||||||
|
const params = { pokemon: target, opponent: user, move: this.move, hitResult, damage };
|
||||||
|
applyAbAttrs("PostDefendAbAttr", params);
|
||||||
|
|
||||||
|
if (wasCritical) {
|
||||||
|
applyAbAttrs("PostReceiveCritStatStageChangeAbAttr", params);
|
||||||
|
}
|
||||||
|
target.lapseTags(BattlerTagLapseType.AFTER_HIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sub-method of {@linkcode applyOnTargetEffects} that handles checking for and applying flinches.
|
||||||
|
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
||||||
|
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
|
||||||
|
* @param dealsDamage - `true` if the attempted move successfully dealt damage
|
||||||
|
*/
|
||||||
|
protected applyHeldItemFlinchCheck(user: Pokemon, target: Pokemon, dealsDamage: boolean): void {
|
||||||
|
if (this.move.hasAttr("FlinchAttr")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
dealsDamage
|
||||||
|
&& !target.hasAbilityWithAttr("IgnoreMoveEffectsAbAttr")
|
||||||
|
&& !this.move.hitsSubstitute(user, target)
|
||||||
|
) {
|
||||||
|
const flinched = new BooleanHolder(false);
|
||||||
|
globalScene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched);
|
||||||
|
if (flinched.value) {
|
||||||
|
target.addTag(BattlerTagType.FLINCHED, undefined, this.move.id, user.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override end(): void {
|
||||||
|
const user = this.getUserPokemon();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this phase isn't for the invoked move's last strike (and we still have something to hit),
|
||||||
|
* unshift another MoveEffectPhase for the next strike before ending this phase.
|
||||||
|
*/
|
||||||
|
if (--user.turnData.hitsLeft >= 1 && this.getFirstTarget()) {
|
||||||
|
this.addNextHitPhase();
|
||||||
|
super.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All hits of the move have resolved by now.
|
||||||
|
* Queue message for multi-strike moves before applying Shell Bell heals & proccing Dancer-like effects.
|
||||||
|
*/
|
||||||
|
const hitsTotal = user.turnData.hitCount - Math.max(user.turnData.hitsLeft, 0);
|
||||||
|
if (hitsTotal > 1 || user.turnData.hitsLeft > 0) {
|
||||||
|
// Queue message if multiple hits occurred or were slated to occur (such as a Triple Axel miss)
|
||||||
|
globalScene.phaseManager.queueMessage(i18next.t("battle:attackHitsCount", { count: hitsTotal }));
|
||||||
|
}
|
||||||
|
|
||||||
|
globalScene.applyModifiers(HitHealModifier, this.player, user);
|
||||||
|
this.getTargets().forEach(target => {
|
||||||
|
target.turnData.moveEffectiveness = null;
|
||||||
|
});
|
||||||
|
super.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// #region Helpers
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns The {@linkcode Pokemon} using this phase's invoked move.
|
||||||
|
* Is never nullish during the move execution itself, as the `start` method
|
||||||
|
* ends the phase immediately if a source is missing.
|
||||||
|
* @todo Delete in favor of {@linkcode PokemonPhase.getPokemon}
|
||||||
|
*/
|
||||||
|
public getUserPokemon(): Pokemon {
|
||||||
|
return super.getPokemon()!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns An array of {@linkcode Pokemon} that are:
|
||||||
|
* - On-field and active
|
||||||
|
* - Non-fainted
|
||||||
|
* - Targeted by this phase's invoked move
|
||||||
|
*/
|
||||||
|
public getTargets(): Pokemon[] {
|
||||||
|
return globalScene.getField(true).filter(p => this.targets.indexOf(p.getBattlerIndex()) > -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns The first active, non-fainted target of this phase's invoked move. */
|
||||||
|
public getFirstTarget(): Pokemon | undefined {
|
||||||
|
return this.getTargets()[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the given {@linkcode Pokemon} from this phase's target list
|
||||||
|
* @param target - The {@linkcode Pokemon} to be removed
|
||||||
|
*/
|
||||||
|
protected removeTarget(target: Pokemon): void {
|
||||||
|
const targetIndex = this.targets.indexOf(target.getBattlerIndex());
|
||||||
|
if (targetIndex !== -1) {
|
||||||
|
this.targets.splice(this.targets.indexOf(target.getBattlerIndex()), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevents subsequent strikes of this phase's invoked move from occurring
|
||||||
|
* @param target - If defined, only stop subsequent strikes against this {@linkcode Pokemon}
|
||||||
|
*/
|
||||||
|
public stopMultiHit(target?: Pokemon): void {
|
||||||
|
// If given a specific target, remove the target from subsequent strikes
|
||||||
|
if (target) {
|
||||||
|
this.removeTarget(target);
|
||||||
|
}
|
||||||
|
const user = this.getUserPokemon();
|
||||||
|
// If no target specified, or the specified target was the last of this move's
|
||||||
|
// targets, completely cancel all subsequent strikes.
|
||||||
|
if (!target || this.targets.length === 0) {
|
||||||
|
user.turnData.hitCount = 1;
|
||||||
|
user.turnData.hitsLeft = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unshifts a new `MoveEffectPhase` with the same properties as this phase.
|
||||||
|
* Used to queue the next hit of multi-strike moves.
|
||||||
|
*/
|
||||||
|
protected addNextHitPhase(): void {
|
||||||
|
globalScene.phaseManager.unshiftNew("MoveEffectPhase", this.battlerIndex, this.targets, this.move, this.useMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Removes all substitutes that were broken by this phase's invoked move */
|
||||||
|
protected updateSubstitutes(): void {
|
||||||
|
const targets = this.getTargets();
|
||||||
|
for (const target of targets) {
|
||||||
|
const substitute = target.getTag(SubstituteTag);
|
||||||
|
if (substitute && substitute.hp <= 0) {
|
||||||
|
target.lapseTag(BattlerTagType.SUBSTITUTE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// # endregion Helpers
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a given Move is able to be reflected by either
|
||||||
|
* {@linkcode MoveId.MAGIC_COAT | Magic Coat} or {@linkcode AbilityId.MAGIC_BOUNCE | Magic Bounce}.
|
||||||
|
* @param move - The {@linkcode Move} being used
|
||||||
|
* @param target - The targeted {@linkcode Pokemon} attempting to reflect the move
|
||||||
|
* @param useMode - The {@linkcode MoveUseMode} dictating how the move was used
|
||||||
|
* @returns Whether {@linkcode target} can reflect {@linkcode move}.
|
||||||
|
*/
|
||||||
|
function isMoveReflectableBy(move: Move, target: Pokemon, useMode: MoveUseMode): boolean {
|
||||||
|
return (
|
||||||
|
// The move must not have just been reflected
|
||||||
|
!isReflected(useMode) // Reflections cannot occur while semi invulnerable
|
||||||
|
&& !target.getTag(SemiInvulnerableTag) // Move must be reflectable
|
||||||
|
&& move.hasFlag(MoveFlags.REFLECTABLE) // target must have a reflection effect active
|
||||||
|
&& (!!target.getTag(BattlerTagType.MAGIC_COAT) || target.hasAbilityWithAttr("ReflectStatusMoveAbAttr"))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
41
src/phases/move-reflect-phase.ts
Normal file
41
src/phases/move-reflect-phase.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { applyAbAttrs } from "#abilities/apply-ab-attrs";
|
||||||
|
import { Phase } from "#app/phase";
|
||||||
|
import type { MagicCoatTag } from "#data/battler-tags";
|
||||||
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
|
// biome-ignore-start lint/correctness/noUnusedImports: TSDoc
|
||||||
|
import type { MoveId } from "#enums/move-id";
|
||||||
|
import type { Pokemon } from "#field/pokemon";
|
||||||
|
import type { Move } from "#types/move-types";
|
||||||
|
// biome-ignore-end lint/correctness/noUnusedImports: TSDoc
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The phase where Pokemon reflect moves via {@linkcode MoveId.MAGIC_COAT | Magic Coat} or {@linkcode AbilityId.MAGIC_BOUNCE | Magic Bounce}.
|
||||||
|
*/
|
||||||
|
export class MoveReflectPhase extends Phase {
|
||||||
|
public override readonly phaseName = "MoveReflectPhase";
|
||||||
|
/** The {@linkcode Pokemon} doing the reflecting. */
|
||||||
|
private readonly pokemon: Pokemon;
|
||||||
|
/** The pokemon having originally used the move. */
|
||||||
|
private opponent: Pokemon;
|
||||||
|
/** The {@linkcode Move} being reflected. */
|
||||||
|
private readonly move: Move;
|
||||||
|
|
||||||
|
constructor(pokemon: Pokemon, opponent: Pokemon, move: Move) {
|
||||||
|
super();
|
||||||
|
this.pokemon = pokemon;
|
||||||
|
this.opponent = opponent;
|
||||||
|
this.move = move;
|
||||||
|
}
|
||||||
|
|
||||||
|
override start(): void {
|
||||||
|
this.pokemon.turnData.extraTurns++;
|
||||||
|
// Magic Coat takes precedeence over Magic Bounce if both apply at once
|
||||||
|
const magicCoatTag = this.pokemon.getTag(BattlerTagType.MAGIC_COAT) as MagicCoatTag | undefined;
|
||||||
|
if (magicCoatTag) {
|
||||||
|
magicCoatTag.apply(this.pokemon, this.opponent, this.move);
|
||||||
|
} else {
|
||||||
|
applyAbAttrs("ReflectStatusMoveAbAttr", { pokemon: this.pokemon, opponent: this.opponent, move: this.move });
|
||||||
|
}
|
||||||
|
super.end();
|
||||||
|
}
|
||||||
|
}
|
@ -1,345 +0,0 @@
|
|||||||
import { allAbilities, allMoves } from "#data/data-lists";
|
|
||||||
import { AbilityId } from "#enums/ability-id";
|
|
||||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
|
||||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
|
||||||
import { BattlerIndex } from "#enums/battler-index";
|
|
||||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
|
||||||
import { MoveId } from "#enums/move-id";
|
|
||||||
import { SpeciesId } from "#enums/species-id";
|
|
||||||
import { Stat } from "#enums/stat";
|
|
||||||
import { StatusEffect } from "#enums/status-effect";
|
|
||||||
import { GameManager } from "#test/test-utils/game-manager";
|
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
describe("Abilities - Magic Bounce", () => {
|
|
||||||
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
|
|
||||||
.ability(AbilityId.BALL_FETCH)
|
|
||||||
.battleStyle("single")
|
|
||||||
.moveset([MoveId.GROWL, MoveId.SPLASH])
|
|
||||||
.criticalHits(false)
|
|
||||||
.enemySpecies(SpeciesId.MAGIKARP)
|
|
||||||
.enemyAbility(AbilityId.MAGIC_BOUNCE)
|
|
||||||
.enemyMoveset(MoveId.SPLASH);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should reflect basic status moves", async () => {
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
game.move.use(MoveId.GROWL);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(game.field.getPlayerPokemon().getStatStage(Stat.ATK)).toBe(-1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not bounce moves while the target is in the semi-invulnerable state", async () => {
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
game.move.use(MoveId.GROWL);
|
|
||||||
await game.move.forceEnemyMove(MoveId.FLY);
|
|
||||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(game.field.getPlayerPokemon().getStatStage(Stat.ATK)).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should individually bounce back multi-target moves", async () => {
|
|
||||||
game.override.battleStyle("double");
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
game.move.use(MoveId.GROWL, 0);
|
|
||||||
game.move.use(MoveId.SPLASH, 1);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
const user = game.field.getPlayerPokemon();
|
|
||||||
expect(user.getStatStage(Stat.ATK)).toBe(-2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should still bounce back a move that would otherwise fail", async () => {
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
game.field.getEnemyPokemon().setStatStage(Stat.ATK, -6);
|
|
||||||
|
|
||||||
game.move.use(MoveId.GROWL);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(game.field.getPlayerPokemon().getStatStage(Stat.ATK)).toBe(-1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not bounce back a move that was just bounced", async () => {
|
|
||||||
game.override.ability(AbilityId.MAGIC_BOUNCE);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.GROWL);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(game.field.getPlayerPokemon().getStatStage(Stat.ATK)).toBe(-1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should receive the stat change after reflecting a move back to a mirror armor user", async () => {
|
|
||||||
game.override.ability(AbilityId.MIRROR_ARMOR);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.GROWL);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(game.field.getEnemyPokemon().getStatStage(Stat.ATK)).toBe(-1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not bounce back a move from a mold breaker user", async () => {
|
|
||||||
game.override.ability(AbilityId.MOLD_BREAKER);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
game.move.use(MoveId.GROWL);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(game.field.getEnemyPokemon().getStatStage(Stat.ATK)).toBe(-1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should bounce back a spread status move against both pokemon", async () => {
|
|
||||||
game.override.battleStyle("double").enemyMoveset([MoveId.SPLASH]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
game.move.use(MoveId.GROWL, 0);
|
|
||||||
game.move.use(MoveId.SPLASH, 1);
|
|
||||||
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(game.scene.getPlayerField().every(p => p.getStatStage(Stat.ATK) === -2)).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should only bounce spikes back once in doubles when both targets have magic bounce", async () => {
|
|
||||||
game.override.battleStyle("double").moveset([MoveId.SPIKES]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.SPIKES);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.PLAYER)!["layers"]).toBe(1);
|
|
||||||
expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY)).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should bounce spikes even when the target is protected", async () => {
|
|
||||||
game.override.moveset([MoveId.SPIKES]).enemyMoveset([MoveId.PROTECT]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.SPIKES);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.PLAYER)!["layers"]).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not bounce spikes when the target is in the semi-invulnerable state", async () => {
|
|
||||||
game.override.moveset([MoveId.SPIKES]).enemyMoveset([MoveId.FLY]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.SPIKES);
|
|
||||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY)!["layers"]).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not bounce back curse", async () => {
|
|
||||||
game.override.moveset([MoveId.CURSE]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.GASTLY]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.CURSE);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(game.field.getEnemyPokemon().getTag(BattlerTagType.CURSED)).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: enable when Magic Bounce is fixed to properly reset the hit count
|
|
||||||
it.todo("should not cause encore to be interrupted after bouncing", async () => {
|
|
||||||
game.override.moveset([MoveId.SPLASH, MoveId.GROWL, MoveId.ENCORE]).enemyMoveset([MoveId.TACKLE, MoveId.GROWL]);
|
|
||||||
// game.override.ability(AbilityId.MOLD_BREAKER);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
const playerPokemon = game.field.getPlayerPokemon();
|
|
||||||
const enemyPokemon = game.field.getEnemyPokemon();
|
|
||||||
|
|
||||||
// Give the player MOLD_BREAKER for this turn to bypass Magic Bounce.
|
|
||||||
const playerAbilitySpy = game.field.mockAbility(playerPokemon, AbilityId.MOLD_BREAKER);
|
|
||||||
|
|
||||||
// turn 1
|
|
||||||
game.move.select(MoveId.ENCORE);
|
|
||||||
await game.move.selectEnemyMove(MoveId.TACKLE);
|
|
||||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
|
||||||
await game.toNextTurn();
|
|
||||||
expect(enemyPokemon.getTag(BattlerTagType.ENCORE)!["moveId"]).toBe(MoveId.TACKLE);
|
|
||||||
|
|
||||||
// turn 2
|
|
||||||
playerAbilitySpy.mockRestore();
|
|
||||||
game.move.select(MoveId.GROWL);
|
|
||||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(enemyPokemon.getTag(BattlerTagType.ENCORE)!["moveId"]).toBe(MoveId.TACKLE);
|
|
||||||
expect(enemyPokemon.getLastXMoves()[0].move).toBe(MoveId.TACKLE);
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: encore is failing if the last move was virtual.
|
|
||||||
it.todo("should not cause the bounced move to count for encore", async () => {
|
|
||||||
game.override
|
|
||||||
.moveset([MoveId.SPLASH, MoveId.GROWL, MoveId.ENCORE])
|
|
||||||
.enemyMoveset([MoveId.GROWL, MoveId.TACKLE])
|
|
||||||
.enemyAbility(AbilityId.MAGIC_BOUNCE);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
const playerPokemon = game.field.getPlayerPokemon();
|
|
||||||
const enemyPokemon = game.field.getEnemyPokemon();
|
|
||||||
|
|
||||||
// turn 1
|
|
||||||
game.move.select(MoveId.GROWL);
|
|
||||||
await game.move.selectEnemyMove(MoveId.TACKLE);
|
|
||||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
// Give the player MOLD_BREAKER for this turn to bypass Magic Bounce.
|
|
||||||
vi.spyOn(playerPokemon, "getAbility").mockReturnValue(allAbilities[AbilityId.MOLD_BREAKER]);
|
|
||||||
|
|
||||||
// turn 2
|
|
||||||
game.move.select(MoveId.ENCORE);
|
|
||||||
await game.move.selectEnemyMove(MoveId.TACKLE);
|
|
||||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(enemyPokemon.getTag(BattlerTagType.ENCORE)!["moveId"]).toBe(MoveId.TACKLE);
|
|
||||||
expect(enemyPokemon.getLastXMoves()[0].move).toBe(MoveId.TACKLE);
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: stomping tantrum should consider moves that were bounced.
|
|
||||||
it.todo("should cause stomping tantrum to double in power when the last move was bounced", async () => {
|
|
||||||
game.override.battleStyle("single").moveset([MoveId.STOMPING_TANTRUM, MoveId.CHARM]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const stomping_tantrum = allMoves[MoveId.STOMPING_TANTRUM];
|
|
||||||
vi.spyOn(stomping_tantrum, "calculateBattlePower");
|
|
||||||
|
|
||||||
game.move.select(MoveId.CHARM);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
game.move.select(MoveId.STOMPING_TANTRUM);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(150);
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: stomping tantrum should consider moves that were bounced
|
|
||||||
it.todo("should boost enemy's stomping tantrum after failed bounce", async () => {
|
|
||||||
game.override.enemyMoveset([MoveId.STOMPING_TANTRUM, MoveId.SPLASH, MoveId.CHARM]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
|
|
||||||
|
|
||||||
const stomping_tantrum = allMoves[MoveId.STOMPING_TANTRUM];
|
|
||||||
const enemy = game.field.getEnemyPokemon();
|
|
||||||
vi.spyOn(stomping_tantrum, "calculateBattlePower");
|
|
||||||
|
|
||||||
// Spore gets reflected back onto us
|
|
||||||
game.move.select(MoveId.SPORE);
|
|
||||||
await game.move.selectEnemyMove(MoveId.CHARM);
|
|
||||||
await game.toNextTurn();
|
|
||||||
expect(enemy.getLastXMoves(1)[0].result).toBe("success");
|
|
||||||
|
|
||||||
game.move.select(MoveId.SPORE);
|
|
||||||
await game.move.selectEnemyMove(MoveId.STOMPING_TANTRUM);
|
|
||||||
await game.toNextTurn();
|
|
||||||
expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(150);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should respect immunities when bouncing a move", async () => {
|
|
||||||
vi.spyOn(allMoves[MoveId.THUNDER_WAVE], "accuracy", "get").mockReturnValue(100);
|
|
||||||
game.override.moveset([MoveId.THUNDER_WAVE, MoveId.GROWL]).ability(AbilityId.SOUNDPROOF);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.PHANPY]);
|
|
||||||
|
|
||||||
// Turn 1 - thunder wave immunity test
|
|
||||||
game.move.select(MoveId.THUNDER_WAVE);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(game.field.getPlayerPokemon().status).toBeUndefined();
|
|
||||||
|
|
||||||
// Turn 2 - soundproof immunity test
|
|
||||||
game.move.select(MoveId.GROWL);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(game.field.getPlayerPokemon().getStatStage(Stat.ATK)).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should bounce back a move before the accuracy check", async () => {
|
|
||||||
game.override.moveset([MoveId.SPORE]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const attacker = game.field.getPlayerPokemon();
|
|
||||||
|
|
||||||
vi.spyOn(attacker, "getAccuracyMultiplier").mockReturnValue(0.0);
|
|
||||||
game.move.select(MoveId.SPORE);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(game.field.getPlayerPokemon().status?.effect).toBe(StatusEffect.SLEEP);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should take the accuracy of the magic bounce user into account", async () => {
|
|
||||||
game.override.moveset([MoveId.SPORE]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
const opponent = game.field.getEnemyPokemon();
|
|
||||||
|
|
||||||
vi.spyOn(opponent, "getAccuracyMultiplier").mockReturnValue(0);
|
|
||||||
game.move.select(MoveId.SPORE);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(game.field.getPlayerPokemon().status).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should always apply the leftmost available target's magic bounce when bouncing moves like sticky webs in doubles", async () => {
|
|
||||||
game.override.battleStyle("double").moveset([MoveId.STICKY_WEB, MoveId.SPLASH, MoveId.TRICK_ROOM]);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]);
|
|
||||||
const [enemy_1, enemy_2] = game.scene.getEnemyField();
|
|
||||||
// set speed just incase logic erroneously checks for speed order
|
|
||||||
enemy_1.setStat(Stat.SPD, enemy_2.getStat(Stat.SPD) + 1);
|
|
||||||
|
|
||||||
// turn 1
|
|
||||||
game.move.select(MoveId.STICKY_WEB, 0);
|
|
||||||
game.move.select(MoveId.TRICK_ROOM, 1);
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
|
|
||||||
expect(
|
|
||||||
game.scene.arena
|
|
||||||
.getTagOnSide(ArenaTagType.STICKY_WEB, ArenaTagSide.PLAYER)
|
|
||||||
?.getSourcePokemon()
|
|
||||||
?.getBattlerIndex(),
|
|
||||||
).toBe(BattlerIndex.ENEMY);
|
|
||||||
game.scene.arena.removeTagOnSide(ArenaTagType.STICKY_WEB, ArenaTagSide.PLAYER, true);
|
|
||||||
|
|
||||||
// turn 2
|
|
||||||
game.move.select(MoveId.STICKY_WEB, 0);
|
|
||||||
game.move.select(MoveId.TRICK_ROOM, 1);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(
|
|
||||||
game.scene.arena
|
|
||||||
.getTagOnSide(ArenaTagType.STICKY_WEB, ArenaTagSide.PLAYER)
|
|
||||||
?.getSourcePokemon()
|
|
||||||
?.getBattlerIndex(),
|
|
||||||
).toBe(BattlerIndex.ENEMY);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not bounce back status moves that hit through semi-invulnerable states", async () => {
|
|
||||||
game.override.moveset([MoveId.TOXIC, MoveId.CHARM]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.TOXIC);
|
|
||||||
await game.move.selectEnemyMove(MoveId.FLY);
|
|
||||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(game.field.getEnemyPokemon().status?.effect).toBe(StatusEffect.TOXIC);
|
|
||||||
expect(game.field.getPlayerPokemon().status).toBeUndefined();
|
|
||||||
|
|
||||||
game.override.ability(AbilityId.NO_GUARD);
|
|
||||||
game.move.select(MoveId.CHARM);
|
|
||||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(game.field.getEnemyPokemon().getStatStage(Stat.ATK)).toBe(-2);
|
|
||||||
expect(game.field.getPlayerPokemon().getStatStage(Stat.ATK)).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
@ -7,8 +7,6 @@ import { GameManager } from "#test/test-utils/game-manager";
|
|||||||
import Phaser from "phaser";
|
import Phaser from "phaser";
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
// TODO: When Magic Bounce is implemented, make a test for its interaction with mirror guard, use screech
|
|
||||||
|
|
||||||
describe("Ability - Mirror Armor", () => {
|
describe("Ability - Mirror Armor", () => {
|
||||||
let phaserGame: Phaser.Game;
|
let phaserGame: Phaser.Game;
|
||||||
let game: GameManager;
|
let game: GameManager;
|
||||||
|
@ -416,5 +416,5 @@ describe("Moves - Delayed Attacks", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Implement and move to a power spot's test file
|
// TODO: Implement and move to a power spot's test file
|
||||||
it.todo("Should activate ally's power spot when switched in during single battles");
|
it.todo("should activate ally's power spot when switched in during single battles");
|
||||||
});
|
});
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
import { BattlerIndex } from "#enums/battler-index";
|
import { BattlerIndex } from "#enums/battler-index";
|
||||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
import { MoveResult } from "#enums/move-result";
|
import { MoveResult } from "#enums/move-result";
|
||||||
|
import { MoveUseMode } from "#enums/move-use-mode";
|
||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
|
import { invalidEncoreMoves } from "#moves/invalid-moves";
|
||||||
import { GameManager } from "#test/test-utils/game-manager";
|
import { GameManager } from "#test/test-utils/game-manager";
|
||||||
|
import i18next from "i18next";
|
||||||
import Phaser from "phaser";
|
import Phaser from "phaser";
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
@ -31,7 +35,6 @@ describe("Moves - Encore", () => {
|
|||||||
.criticalHits(false)
|
.criticalHits(false)
|
||||||
.enemySpecies(SpeciesId.MAGIKARP)
|
.enemySpecies(SpeciesId.MAGIKARP)
|
||||||
.enemyAbility(AbilityId.BALL_FETCH)
|
.enemyAbility(AbilityId.BALL_FETCH)
|
||||||
.enemyMoveset([MoveId.SPLASH, MoveId.TACKLE])
|
|
||||||
.startingLevel(100)
|
.startingLevel(100)
|
||||||
.enemyLevel(100);
|
.enemyLevel(100);
|
||||||
});
|
});
|
||||||
@ -39,76 +42,154 @@ describe("Moves - Encore", () => {
|
|||||||
it("should prevent the target from using any move except the last used move", async () => {
|
it("should prevent the target from using any move except the last used move", async () => {
|
||||||
await game.classicMode.startBattle([SpeciesId.SNORLAX]);
|
await game.classicMode.startBattle([SpeciesId.SNORLAX]);
|
||||||
|
|
||||||
const enemyPokemon = game.field.getEnemyPokemon();
|
const enemy = game.field.getEnemyPokemon();
|
||||||
|
|
||||||
game.move.select(MoveId.ENCORE);
|
game.move.use(MoveId.ENCORE);
|
||||||
await game.move.selectEnemyMove(MoveId.SPLASH);
|
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||||
|
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(enemy).toHaveBattlerTag(BattlerTagType.ENCORE);
|
||||||
|
expect(enemy.isMoveRestricted(MoveId.TACKLE)).toBe(true);
|
||||||
|
expect(enemy.isMoveRestricted(MoveId.SPLASH)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be removed on turn end after triggering thrice, ignoring Instruct", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.SNORLAX]);
|
||||||
|
|
||||||
|
const enemy = game.field.getEnemyPokemon();
|
||||||
|
enemy.pushMoveHistory({ move: MoveId.SPLASH, targets: [BattlerIndex.PLAYER], useMode: MoveUseMode.NORMAL });
|
||||||
|
|
||||||
|
game.move.use(MoveId.ENCORE);
|
||||||
|
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||||
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
// Should have ticked down once
|
||||||
|
expect(enemy).toHaveBattlerTag(BattlerTagType.ENCORE);
|
||||||
|
expect(enemy.getTag(BattlerTagType.ENCORE)!.turnCount).toBe(2);
|
||||||
|
|
||||||
|
game.move.use(MoveId.INSTRUCT);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(enemy.getTag(BattlerTagType.ENCORE)!.turnCount).toBe(1);
|
||||||
|
|
||||||
|
game.move.use(MoveId.INSTRUCT);
|
||||||
|
await game.toEndOfTurn(false);
|
||||||
|
|
||||||
|
// Tag should still be present until the `TurnEndPhase` ticks it down
|
||||||
|
expect(enemy).toHaveBattlerTag(BattlerTagType.ENCORE);
|
||||||
|
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
expect(enemyPokemon.getTag(BattlerTagType.ENCORE)).toBeDefined();
|
|
||||||
|
|
||||||
game.move.select(MoveId.SPLASH);
|
expect(enemy).not.toHaveBattlerTag(BattlerTagType.ENCORE);
|
||||||
// The enemy AI would normally be inclined to use Tackle, but should be
|
expect(game.textInterceptor.logs).toContain(
|
||||||
// forced into using Splash.
|
i18next.t("battlerTags:encoreOnRemove", {
|
||||||
await game.phaseInterceptor.to("BerryPhase", false);
|
pokemonNameWithAffix: getPokemonNameWithAffix(enemy),
|
||||||
|
}),
|
||||||
expect(enemyPokemon.getLastXMoves().every(turnMove => turnMove.move === MoveId.SPLASH)).toBeTruthy();
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("should fail against the following moves:", () => {
|
it("should override any upcoming moves with the Encored move, while still consuming PP", async () => {
|
||||||
it.each([
|
await game.classicMode.startBattle([SpeciesId.SNORLAX]);
|
||||||
{ moveId: MoveId.TRANSFORM, name: "Transform", delay: false },
|
|
||||||
{ moveId: MoveId.MIMIC, name: "Mimic", delay: true },
|
|
||||||
{ moveId: MoveId.SKETCH, name: "Sketch", delay: true },
|
|
||||||
{ moveId: MoveId.ENCORE, name: "Encore", delay: false },
|
|
||||||
{ moveId: MoveId.STRUGGLE, name: "Struggle", delay: false },
|
|
||||||
])("$name", async ({ moveId, delay }) => {
|
|
||||||
game.override.enemyMoveset(moveId);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.SNORLAX]);
|
// Fake enemy having used tackle the turn prior
|
||||||
|
const enemy = game.field.getEnemyPokemon();
|
||||||
|
game.move.changeMoveset(enemy, [MoveId.SPLASH, MoveId.TACKLE]);
|
||||||
|
enemy.pushMoveHistory({ move: MoveId.TACKLE, targets: [BattlerIndex.PLAYER], useMode: MoveUseMode.NORMAL });
|
||||||
|
|
||||||
const playerPokemon = game.field.getPlayerPokemon();
|
game.move.use(MoveId.ENCORE);
|
||||||
const enemyPokemon = game.field.getEnemyPokemon();
|
await game.move.selectEnemyMove(MoveId.SPLASH);
|
||||||
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
if (delay) {
|
expect(enemy).toHaveUsedMove({ move: MoveId.TACKLE, targets: [BattlerIndex.PLAYER], useMode: MoveUseMode.NORMAL });
|
||||||
game.move.select(MoveId.SPLASH);
|
expect(enemy).toHaveUsedPP(MoveId.TACKLE, 1);
|
||||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
|
||||||
await game.toNextTurn();
|
|
||||||
}
|
|
||||||
|
|
||||||
game.move.select(MoveId.ENCORE);
|
|
||||||
|
|
||||||
const turnOrder = delay ? [BattlerIndex.PLAYER, BattlerIndex.ENEMY] : [BattlerIndex.ENEMY, BattlerIndex.PLAYER];
|
|
||||||
await game.setTurnOrder(turnOrder);
|
|
||||||
|
|
||||||
await game.phaseInterceptor.to("BerryPhase", false);
|
|
||||||
expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL);
|
|
||||||
expect(enemyPokemon.getTag(BattlerTagType.ENCORE)).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Pokemon under both Encore and Torment should alternate between Struggle and restricted move", async () => {
|
// TODO: Make test using `changeMoveset`
|
||||||
const turnOrder = [BattlerIndex.ENEMY, BattlerIndex.PLAYER];
|
it.todo("should end at turn end if the user forgets the Encored move");
|
||||||
game.override.moveset([MoveId.ENCORE, MoveId.TORMENT, MoveId.SPLASH]);
|
|
||||||
|
it("should be removed at turn end if the Encored move runs out of PP", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.SNORLAX]);
|
||||||
|
|
||||||
|
// Fake enemy having used tackle the turn prior
|
||||||
|
const enemy = game.field.getEnemyPokemon();
|
||||||
|
game.move.changeMoveset(enemy, [MoveId.SPLASH, MoveId.TACKLE]);
|
||||||
|
enemy.pushMoveHistory({ move: MoveId.TACKLE, targets: [BattlerIndex.PLAYER], useMode: MoveUseMode.NORMAL });
|
||||||
|
enemy.moveset[1].ppUsed = enemy.moveset[1].getMovePp() - 2;
|
||||||
|
|
||||||
|
game.move.use(MoveId.ENCORE);
|
||||||
|
await game.move.selectEnemyMove(MoveId.SPLASH);
|
||||||
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(enemy).toHaveUsedMove({ move: MoveId.TACKLE, targets: [BattlerIndex.PLAYER], useMode: MoveUseMode.NORMAL });
|
||||||
|
expect(enemy).toHaveUsedPP(MoveId.TACKLE, enemy.moveset[1].getMovePp() - 1);
|
||||||
|
expect(enemy).toHaveBattlerTag(BattlerTagType.ENCORE);
|
||||||
|
|
||||||
|
game.move.use(MoveId.SPLASH);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(enemy).toHaveUsedPP(MoveId.TACKLE, "all");
|
||||||
|
expect(enemy).not.toHaveBattlerTag(BattlerTagType.ENCORE);
|
||||||
|
});
|
||||||
|
|
||||||
|
const invalidMoves = [...invalidEncoreMoves].map(m => ({
|
||||||
|
name: MoveId[m],
|
||||||
|
move: m,
|
||||||
|
}));
|
||||||
|
it.each(invalidMoves)("should fail if the target's last move is $name", async ({ move }) => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.SNORLAX]);
|
||||||
|
|
||||||
|
const player = game.field.getPlayerPokemon();
|
||||||
|
const enemy = game.field.getEnemyPokemon();
|
||||||
|
enemy.pushMoveHistory({ move, targets: [BattlerIndex.PLAYER], useMode: MoveUseMode.NORMAL });
|
||||||
|
|
||||||
|
game.move.use(MoveId.ENCORE);
|
||||||
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL);
|
||||||
|
expect(enemy).not.toHaveBattlerTag(BattlerTagType.ENCORE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail if the target has not made a move", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.SNORLAX]);
|
||||||
|
|
||||||
|
const player = game.field.getPlayerPokemon();
|
||||||
|
const enemy = game.field.getEnemyPokemon();
|
||||||
|
|
||||||
|
game.move.use(MoveId.ENCORE);
|
||||||
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL);
|
||||||
|
expect(enemy).not.toHaveBattlerTag(BattlerTagType.ENCORE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should force a Tormented target to alternate between Struggle and the Encored move", async () => {
|
||||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||||
|
|
||||||
const enemyPokemon = game.field.getEnemyPokemon();
|
const enemy = game.field.getEnemyPokemon();
|
||||||
game.move.select(MoveId.ENCORE);
|
|
||||||
await game.setTurnOrder(turnOrder);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(enemyPokemon.getTag(BattlerTagType.ENCORE)).toBeDefined();
|
|
||||||
|
|
||||||
|
game.move.use(MoveId.ENCORE);
|
||||||
|
await game.move.forceEnemyMove(MoveId.TACKLE);
|
||||||
|
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
game.move.select(MoveId.TORMENT);
|
|
||||||
await game.setTurnOrder(turnOrder);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(enemyPokemon.getTag(BattlerTagType.TORMENT)).toBeDefined();
|
|
||||||
|
|
||||||
|
expect(enemy).toHaveBattlerTag(BattlerTagType.ENCORE);
|
||||||
|
|
||||||
|
game.move.use(MoveId.TORMENT);
|
||||||
|
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
game.move.select(MoveId.SPLASH);
|
|
||||||
await game.setTurnOrder(turnOrder);
|
expect(enemy).toHaveBattlerTag(BattlerTagType.ENCORE);
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
expect(enemy).toHaveBattlerTag(BattlerTagType.TORMENT);
|
||||||
const lastMove = enemyPokemon.getLastXMoves()[0];
|
|
||||||
expect(lastMove?.move).toBe(MoveId.STRUGGLE);
|
game.move.use(MoveId.SPLASH);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(enemy).toHaveUsedMove(MoveId.STRUGGLE);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
390
test/moves/magic-coat-magic-bounce.test.ts
Normal file
390
test/moves/magic-coat-magic-bounce.test.ts
Normal file
@ -0,0 +1,390 @@
|
|||||||
|
import { allMoves } from "#data/data-lists";
|
||||||
|
import { AbilityId } from "#enums/ability-id";
|
||||||
|
import { ArenaTagSide } from "#enums/arena-tag-side";
|
||||||
|
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||||
|
import { BattlerIndex } from "#enums/battler-index";
|
||||||
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
|
import { MoveId } from "#enums/move-id";
|
||||||
|
import { MoveResult } from "#enums/move-result";
|
||||||
|
import { MoveUseMode } from "#enums/move-use-mode";
|
||||||
|
import { SpeciesId } from "#enums/species-id";
|
||||||
|
import { Stat } from "#enums/stat";
|
||||||
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
|
import { GameManager } from "#test/test-utils/game-manager";
|
||||||
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
describe("Moves - Reflecting effects", () => {
|
||||||
|
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
|
||||||
|
.ability(AbilityId.BALL_FETCH)
|
||||||
|
.battleStyle("single")
|
||||||
|
.criticalHits(false)
|
||||||
|
.enemySpecies(SpeciesId.MAGIKARP)
|
||||||
|
.enemyAbility(AbilityId.BALL_FETCH)
|
||||||
|
.enemyMoveset(MoveId.MAGIC_COAT);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Reflecting effects", () => {
|
||||||
|
it("should reflect basic status moves, copying them against the user", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
|
game.move.use(MoveId.GROWL);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
const player = game.field.getPlayerPokemon();
|
||||||
|
const enemy = game.field.getEnemyPokemon();
|
||||||
|
|
||||||
|
expect(enemy).toHaveUsedMove({
|
||||||
|
move: MoveId.GROWL,
|
||||||
|
useMode: MoveUseMode.REFLECTED,
|
||||||
|
targets: [BattlerIndex.PLAYER],
|
||||||
|
});
|
||||||
|
expect(player).toHaveStatStage(Stat.ATK, -1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should bounce back multi-target moves against each target", async () => {
|
||||||
|
game.override.battleStyle("double");
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
|
game.move.use(MoveId.GROWL, BattlerIndex.PLAYER);
|
||||||
|
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
const [karp1, karp2] = game.scene.getPlayerField();
|
||||||
|
expect(karp1).toHaveStatStage(Stat.ATK, -2);
|
||||||
|
expect(karp2).toHaveStatStage(Stat.ATK, -2);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: This is broken - failed moves never make it to a MEP
|
||||||
|
it.todo("should still bounce back a move that would otherwise fail", async () => {
|
||||||
|
game.override.enemyAbility(AbilityId.INSOMNIA);
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
|
game.move.use(MoveId.YAWN);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(game.field.getPlayerPokemon()).toHaveBattlerTag(BattlerTagType.DROWSY);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not bounce back a move that was just bounced", async () => {
|
||||||
|
game.override.battleStyle("double").ability(AbilityId.MAGIC_BOUNCE);
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
|
game.move.use(MoveId.MAGIC_COAT, BattlerIndex.PLAYER);
|
||||||
|
game.move.use(MoveId.GROWL, BattlerIndex.PLAYER_2);
|
||||||
|
await game.move.forceEnemyMove(MoveId.MAGIC_COAT);
|
||||||
|
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(game.field.getEnemyPokemon()).toHaveStatStage(Stat.ATK, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should take precedence over Mirror Armor", async () => {
|
||||||
|
game.override.enemyAbility(AbilityId.MIRROR_ARMOR);
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
|
game.move.use(MoveId.GROWL);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
const enemy = game.field.getPlayerPokemon();
|
||||||
|
expect(enemy).toHaveStatStage(Stat.ATK, -1);
|
||||||
|
expect(enemy).not.toHaveAbilityApplied(AbilityId.MIRROR_ARMOR);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not bounce back non-reflectable effects", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.GASTLY]);
|
||||||
|
|
||||||
|
game.move.use(MoveId.CURSE);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(game.field.getEnemyPokemon()).toHaveBattlerTag(BattlerTagType.CURSED);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not cause encore to be interrupted after bouncing", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
|
const playerPokemon = game.field.getPlayerPokemon();
|
||||||
|
const enemyPokemon = game.field.getEnemyPokemon();
|
||||||
|
|
||||||
|
// Give the player MOLD_BREAKER for this turn to bypass Magic Bounce.
|
||||||
|
const playerAbilitySpy = game.field.mockAbility(playerPokemon, AbilityId.MOLD_BREAKER);
|
||||||
|
|
||||||
|
// turn 1
|
||||||
|
game.move.use(MoveId.ENCORE);
|
||||||
|
await game.move.forceEnemyMove(MoveId.TACKLE);
|
||||||
|
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(enemyPokemon.getTag(BattlerTagType.ENCORE)!["moveId"]).toBe(MoveId.TACKLE);
|
||||||
|
|
||||||
|
// turn 2
|
||||||
|
playerAbilitySpy.mockRestore();
|
||||||
|
|
||||||
|
game.move.use(MoveId.GROWL);
|
||||||
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(enemyPokemon.getTag(BattlerTagType.ENCORE)!["moveId"]).toBe(MoveId.TACKLE);
|
||||||
|
expect(enemyPokemon.getLastXMoves()[0].move).toBe(MoveId.TACKLE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not cause the bounced move to count for encore", async () => {
|
||||||
|
game.override.battleStyle("double");
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.ABRA]);
|
||||||
|
|
||||||
|
// Fake abra having mold breaker and the enemy having used Tackle
|
||||||
|
const [, abra, enemy1] = game.scene.getField();
|
||||||
|
game.field.mockAbility(abra, AbilityId.MOLD_BREAKER);
|
||||||
|
game.field.mockAbility(enemy1, AbilityId.MAGIC_BOUNCE);
|
||||||
|
game.move.changeMoveset(enemy1, [MoveId.TACKLE, MoveId.SPLASH]);
|
||||||
|
enemy1.pushMoveHistory({ move: MoveId.TACKLE, targets: [BattlerIndex.PLAYER], useMode: MoveUseMode.NORMAL });
|
||||||
|
|
||||||
|
// Magikarp uses growl as Abra attempts to encore enemy 1
|
||||||
|
game.move.use(MoveId.GROWL, BattlerIndex.PLAYER);
|
||||||
|
game.move.use(MoveId.ENCORE, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY);
|
||||||
|
await game.move.selectEnemyMove(MoveId.SPLASH);
|
||||||
|
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||||
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
console.log(enemy1.getLastXMoves(-1));
|
||||||
|
// Encore locked into Tackle, replacing the enemy's Growl with another Tackle
|
||||||
|
expect(enemy1.getTag(BattlerTagType.ENCORE)?.["moveId"]).toBe(MoveId.TACKLE);
|
||||||
|
expect(enemy1).toHaveUsedMove({ move: MoveId.TACKLE, useMode: MoveUseMode.NORMAL });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should boost stomping tantrum after a failed bounce", async () => {
|
||||||
|
game.override.ability(AbilityId.INSOMNIA);
|
||||||
|
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
|
||||||
|
|
||||||
|
const enemy = game.field.getEnemyPokemon();
|
||||||
|
const powerSpy = vi.spyOn(allMoves[MoveId.STOMPING_TANTRUM], "calculateBattlePower");
|
||||||
|
|
||||||
|
// Yawn gets reflected back onto us, failing due to Insomnia
|
||||||
|
game.move.use(MoveId.YAWN);
|
||||||
|
await game.move.forceEnemyMove(MoveId.MAGIC_COAT);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(enemy).toHaveUsedMove({ move: MoveId.YAWN, result: MoveResult.FAIL, useMode: MoveUseMode.REFLECTED });
|
||||||
|
|
||||||
|
game.move.use(MoveId.SPLASH);
|
||||||
|
await game.move.forceEnemyMove(MoveId.STOMPING_TANTRUM);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(powerSpy).toHaveReturnedWith(150);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should respect immunities when bouncing a move", async () => {
|
||||||
|
vi.spyOn(allMoves[MoveId.THUNDER_WAVE], "accuracy", "get").mockReturnValue(100);
|
||||||
|
game.override.ability(AbilityId.SOUNDPROOF);
|
||||||
|
await game.classicMode.startBattle([SpeciesId.PHANPY]);
|
||||||
|
|
||||||
|
// Turn 1 - thunder wave immunity test
|
||||||
|
game.move.use(MoveId.THUNDER_WAVE);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
expect(game.field.getPlayerPokemon().status).toBeUndefined();
|
||||||
|
|
||||||
|
// Turn 2 - soundproof immunity test
|
||||||
|
game.move.use(MoveId.GROWL);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
expect(game.field.getPlayerPokemon()).toHaveStatStage(Stat.ATK, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should ignore the original move's accuracy and use the user's accuracy", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
|
const magikarp = game.field.getPlayerPokemon();
|
||||||
|
const feebas = game.field.getEnemyPokemon();
|
||||||
|
const karpMissSpy = vi.spyOn(magikarp, "getAccuracyMultiplier").mockReturnValue(0);
|
||||||
|
|
||||||
|
// Turn 1: Force a miss on initial move
|
||||||
|
game.move.use(MoveId.SPORE);
|
||||||
|
await game.phaseInterceptor.to("MoveEndPhase");
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
// todo change once matchers fixed
|
||||||
|
expect(magikarp.status?.effect).toBe(StatusEffect.SLEEP);
|
||||||
|
|
||||||
|
magikarp.clearStatus(false, false);
|
||||||
|
|
||||||
|
karpMissSpy.mockRestore();
|
||||||
|
vi.spyOn(feebas, "getAccuracyMultiplier").mockReturnValue(0);
|
||||||
|
|
||||||
|
// Turn 2: Force a miss on Feebas' reflected move
|
||||||
|
game.move.use(MoveId.SPORE);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(magikarp.status?.effect).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Magic Bounce", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
game.override.enemyAbility(AbilityId.MAGIC_BOUNCE).enemyMoveset(MoveId.SPLASH);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Change post speed order rework to check the FASTER pokemon's ability
|
||||||
|
it("should only apply the leftmost available target's magic bounce when bouncing field-targeted moves in doubles", async () => {
|
||||||
|
game.override.battleStyle("double");
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
|
const [enemy1, enemy2] = game.scene.getEnemyField();
|
||||||
|
// set speed to different values just in case logic erroneously checks for speed order
|
||||||
|
enemy1.setStat(Stat.SPD, enemy2.getStat(Stat.SPD) + 1);
|
||||||
|
|
||||||
|
// turn 1
|
||||||
|
game.move.use(MoveId.SPIKES, 0);
|
||||||
|
game.move.use(MoveId.TRICK_ROOM, 1);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
// TODO: Replace this with `expect(game).toHaveArenaTag({tagType: ArenaTagType.SPIKES, side: ArenaTagSide.PLAYER, sourceId: enemy1.id, layers: 1})
|
||||||
|
const tag = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.PLAYER)!;
|
||||||
|
expect(tag).toBeDefined();
|
||||||
|
expect(tag.getSourcePokemon()).toBe(enemy1);
|
||||||
|
expect(tag["layers"]).toBe(1);
|
||||||
|
game.scene.arena.removeTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.PLAYER, true);
|
||||||
|
|
||||||
|
// turn 2
|
||||||
|
game.move.use(MoveId.SPIKES, 0);
|
||||||
|
game.move.use(MoveId.TRICK_ROOM, 1);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
// TODO: Replace this with `expect(game).toHaveArenaTag({tagType: ArenaTagType.SPIKES, side: ArenaTagSide.PLAYER, sourceId: enemy1.id, layers: 1})
|
||||||
|
expect(
|
||||||
|
game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.PLAYER)?.getSourcePokemon()?.getBattlerIndex(),
|
||||||
|
).toBe(BattlerIndex.ENEMY);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not bounce back status moves against semi-invulnerable Pokemon, even with No Guard", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
|
||||||
|
|
||||||
|
const player = game.field.getPlayerPokemon();
|
||||||
|
const enemy = game.field.getEnemyPokemon();
|
||||||
|
|
||||||
|
// Turn 1: use charm while enemy is airborne; misses
|
||||||
|
game.move.use(MoveId.CHARM);
|
||||||
|
await game.move.forceEnemyMove(MoveId.FLY);
|
||||||
|
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(player).toHaveStatStage(Stat.ATK, 0);
|
||||||
|
expect(enemy).toHaveStatStage(Stat.ATK, 0);
|
||||||
|
|
||||||
|
// Turn 2: Use Charm through No Guard; should not be reflected
|
||||||
|
game.field.mockAbility(player, AbilityId.NO_GUARD);
|
||||||
|
|
||||||
|
game.move.use(MoveId.CHARM);
|
||||||
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(player).toHaveStatStage(Stat.ATK, 0);
|
||||||
|
expect(enemy).toHaveStatStage(Stat.ATK, -2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be overridden by Magic Coat without stacking", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
|
const karp = game.field.getPlayerPokemon();
|
||||||
|
|
||||||
|
game.move.use(MoveId.GROWL);
|
||||||
|
await game.move.forceEnemyMove(MoveId.MAGIC_COAT);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(karp).toHaveStatStage(Stat.ATK, -1);
|
||||||
|
expect(game.field.getEnemyPokemon()).not.toHaveAbilityApplied(AbilityId.MAGIC_BOUNCE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should bounce spikes even when the target is protected", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
|
game.move.use(MoveId.SPIKES);
|
||||||
|
await game.move.forceEnemyMove(MoveId.PROTECT);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
// TODO: Replace this with `expect(game).toHaveArenaTag({tagType: ArenaTagType.SPIKES, side: ArenaTagSide.PLAYER, layers: 1})
|
||||||
|
expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.PLAYER)!["layers"]).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not break subsequent multi-strike moves", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.PALKIA]);
|
||||||
|
|
||||||
|
game.move.use(MoveId.GROWL);
|
||||||
|
await game.move.forceEnemyMove(MoveId.SURGING_STRIKES);
|
||||||
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
const enemy = game.field.getEnemyPokemon();
|
||||||
|
expect(enemy.turnData.hitCount).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Magic Coat", () => {
|
||||||
|
it("should fail if the user goes last in the turn", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
|
game.move.use(MoveId.PROTECT);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(game.field.getEnemyPokemon().getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail if called again in the same turn from Instruct", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
|
game.move.use(MoveId.INSTRUCT);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
expect(game.field.getEnemyPokemon().getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not reflect moves used on the next turn", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
|
// turn 1
|
||||||
|
game.move.use(MoveId.SPLASH);
|
||||||
|
await game.move.forceEnemyMove(MoveId.MAGIC_COAT);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
// turn 2
|
||||||
|
game.move.use(MoveId.GROWL);
|
||||||
|
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
expect(game.field.getEnemyPokemon()).toHaveStatStage(Stat.ATK, -1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should still bounce back a move from a mold breaker user", async () => {
|
||||||
|
game.override.ability(AbilityId.MOLD_BREAKER).moveset([MoveId.GROWL]);
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
|
game.move.use(MoveId.GROWL);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(game.field.getEnemyPokemon()).toHaveStatStage(Stat.ATK, 0);
|
||||||
|
expect(game.field.getPlayerPokemon()).toHaveStatStage(Stat.ATK, -1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should only bounce spikes back once when both targets use magic coat in doubles", async () => {
|
||||||
|
game.override.battleStyle("double");
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
|
game.move.use(MoveId.SPIKES);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.PLAYER)!["layers"]).toBe(1);
|
||||||
|
expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY)).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,285 +0,0 @@
|
|||||||
import { allMoves } from "#data/data-lists";
|
|
||||||
import { AbilityId } from "#enums/ability-id";
|
|
||||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
|
||||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
|
||||||
import { BattlerIndex } from "#enums/battler-index";
|
|
||||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
|
||||||
import { MoveId } from "#enums/move-id";
|
|
||||||
import { MoveResult } from "#enums/move-result";
|
|
||||||
import { SpeciesId } from "#enums/species-id";
|
|
||||||
import { Stat } from "#enums/stat";
|
|
||||||
import { StatusEffect } from "#enums/status-effect";
|
|
||||||
import { GameManager } from "#test/test-utils/game-manager";
|
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
describe("Moves - Magic Coat", () => {
|
|
||||||
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
|
|
||||||
.ability(AbilityId.BALL_FETCH)
|
|
||||||
.battleStyle("single")
|
|
||||||
.criticalHits(false)
|
|
||||||
.enemySpecies(SpeciesId.MAGIKARP)
|
|
||||||
.enemyAbility(AbilityId.BALL_FETCH)
|
|
||||||
.enemyMoveset(MoveId.MAGIC_COAT);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should fail if the user goes last in the turn", async () => {
|
|
||||||
game.override.moveset([MoveId.PROTECT]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.PROTECT);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(game.field.getEnemyPokemon().getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should fail if called again in the same turn due to moves like instruct", async () => {
|
|
||||||
game.override.moveset([MoveId.INSTRUCT]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.INSTRUCT);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(game.field.getEnemyPokemon().getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not reflect moves used on the next turn", async () => {
|
|
||||||
game.override.moveset([MoveId.GROWL, MoveId.SPLASH]).enemyMoveset([MoveId.MAGIC_COAT, MoveId.SPLASH]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
// turn 1
|
|
||||||
game.move.select(MoveId.SPLASH);
|
|
||||||
await game.move.selectEnemyMove(MoveId.MAGIC_COAT);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
// turn 2
|
|
||||||
game.move.select(MoveId.GROWL);
|
|
||||||
await game.move.selectEnemyMove(MoveId.SPLASH);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(game.field.getEnemyPokemon().getStatStage(Stat.ATK)).toBe(-1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should reflect basic status moves", async () => {
|
|
||||||
game.override.moveset([MoveId.GROWL]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.GROWL);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(game.field.getPlayerPokemon().getStatStage(Stat.ATK)).toBe(-1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should individually bounce back multi-target moves when used by both targets in doubles", async () => {
|
|
||||||
game.override.battleStyle("double").moveset([MoveId.GROWL, MoveId.SPLASH]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.GROWL, 0);
|
|
||||||
game.move.select(MoveId.SPLASH, 1);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
const user = game.scene.getPlayerField()[0];
|
|
||||||
expect(user.getStatStage(Stat.ATK)).toBe(-2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should bounce back a spread status move against both pokemon", async () => {
|
|
||||||
game.override
|
|
||||||
.battleStyle("double")
|
|
||||||
.moveset([MoveId.GROWL, MoveId.SPLASH])
|
|
||||||
.enemyMoveset([MoveId.SPLASH, MoveId.MAGIC_COAT]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.GROWL, 0);
|
|
||||||
game.move.select(MoveId.SPLASH, 1);
|
|
||||||
await game.move.selectEnemyMove(MoveId.SPLASH);
|
|
||||||
await game.move.selectEnemyMove(MoveId.MAGIC_COAT);
|
|
||||||
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(game.scene.getPlayerField().every(p => p.getStatStage(Stat.ATK) === -1)).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should still bounce back a move that would otherwise fail", async () => {
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
game.field.getEnemyPokemon().setStatStage(Stat.ATK, -6);
|
|
||||||
game.override.moveset([MoveId.GROWL]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.GROWL);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(game.field.getPlayerPokemon().getStatStage(Stat.ATK)).toBe(-1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not bounce back a move that was just bounced", async () => {
|
|
||||||
game.override
|
|
||||||
.battleStyle("double")
|
|
||||||
.ability(AbilityId.MAGIC_BOUNCE)
|
|
||||||
.moveset([MoveId.GROWL, MoveId.MAGIC_COAT])
|
|
||||||
.enemyMoveset([MoveId.SPLASH, MoveId.MAGIC_COAT]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.MAGIC_COAT, 0);
|
|
||||||
game.move.select(MoveId.GROWL, 1);
|
|
||||||
await game.move.selectEnemyMove(MoveId.MAGIC_COAT);
|
|
||||||
await game.move.selectEnemyMove(MoveId.SPLASH);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(game.scene.getEnemyField()[0].getStatStage(Stat.ATK)).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
// todo while Mirror Armor is not implemented
|
|
||||||
it.todo("should receive the stat change after reflecting a move back to a mirror armor user", async () => {
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.GROWL);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(game.field.getEnemyPokemon().getStatStage(Stat.ATK)).toBe(-1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should still bounce back a move from a mold breaker user", async () => {
|
|
||||||
game.override.ability(AbilityId.MOLD_BREAKER).moveset([MoveId.GROWL]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.GROWL);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(game.field.getEnemyPokemon().getStatStage(Stat.ATK)).toBe(0);
|
|
||||||
expect(game.field.getPlayerPokemon().getStatStage(Stat.ATK)).toBe(-1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should only bounce spikes back once when both targets use magic coat in doubles", async () => {
|
|
||||||
game.override.battleStyle("double").moveset([MoveId.SPIKES]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.SPIKES);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.PLAYER)!["layers"]).toBe(1);
|
|
||||||
expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY)).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not bounce back curse", async () => {
|
|
||||||
game.override.moveset([MoveId.CURSE]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.GASTLY]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.CURSE);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(game.field.getEnemyPokemon().getTag(BattlerTagType.CURSED)).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: encore is failing if the last move was virtual.
|
|
||||||
it.todo("should not cause the bounced move to count for encore", async () => {
|
|
||||||
game.override
|
|
||||||
.moveset([MoveId.GROWL, MoveId.ENCORE])
|
|
||||||
.enemyMoveset([MoveId.MAGIC_COAT, MoveId.TACKLE])
|
|
||||||
.enemyAbility(AbilityId.MAGIC_BOUNCE);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
const enemyPokemon = game.field.getEnemyPokemon();
|
|
||||||
|
|
||||||
// turn 1
|
|
||||||
game.move.select(MoveId.GROWL);
|
|
||||||
await game.move.selectEnemyMove(MoveId.MAGIC_COAT);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
// turn 2
|
|
||||||
game.move.select(MoveId.ENCORE);
|
|
||||||
await game.move.selectEnemyMove(MoveId.TACKLE);
|
|
||||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(enemyPokemon.getTag(BattlerTagType.ENCORE)!["moveId"]).toBe(MoveId.TACKLE);
|
|
||||||
expect(enemyPokemon.getLastXMoves()[0].move).toBe(MoveId.TACKLE);
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: stomping tantrum should consider moves that were bounced.
|
|
||||||
it.todo("should cause stomping tantrum to double in power when the last move was bounced", async () => {
|
|
||||||
game.override.battleStyle("single").moveset([MoveId.STOMPING_TANTRUM, MoveId.CHARM]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const stomping_tantrum = allMoves[MoveId.STOMPING_TANTRUM];
|
|
||||||
vi.spyOn(stomping_tantrum, "calculateBattlePower");
|
|
||||||
|
|
||||||
game.move.select(MoveId.CHARM);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
game.move.select(MoveId.STOMPING_TANTRUM);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(150);
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: stomping tantrum should consider moves that were bounced.
|
|
||||||
it.todo(
|
|
||||||
"should properly cause the enemy's stomping tantrum to be doubled in power after bouncing and failing",
|
|
||||||
async () => {
|
|
||||||
game.override.enemyMoveset([MoveId.STOMPING_TANTRUM, MoveId.SPLASH, MoveId.CHARM]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
|
|
||||||
|
|
||||||
const stomping_tantrum = allMoves[MoveId.STOMPING_TANTRUM];
|
|
||||||
const enemy = game.field.getEnemyPokemon();
|
|
||||||
vi.spyOn(stomping_tantrum, "calculateBattlePower");
|
|
||||||
|
|
||||||
game.move.select(MoveId.SPORE);
|
|
||||||
await game.move.selectEnemyMove(MoveId.CHARM);
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
expect(enemy.getLastXMoves(1)[0].result).toBe("success");
|
|
||||||
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(75);
|
|
||||||
|
|
||||||
await game.toNextTurn();
|
|
||||||
game.move.select(MoveId.GROWL);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(75);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
it("should respect immunities when bouncing a move", async () => {
|
|
||||||
vi.spyOn(allMoves[MoveId.THUNDER_WAVE], "accuracy", "get").mockReturnValue(100);
|
|
||||||
game.override.moveset([MoveId.THUNDER_WAVE, MoveId.GROWL]).ability(AbilityId.SOUNDPROOF);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.PHANPY]);
|
|
||||||
|
|
||||||
// Turn 1 - thunder wave immunity test
|
|
||||||
game.move.select(MoveId.THUNDER_WAVE);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(game.field.getPlayerPokemon().status).toBeUndefined();
|
|
||||||
|
|
||||||
// Turn 2 - soundproof immunity test
|
|
||||||
game.move.select(MoveId.GROWL);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(game.field.getPlayerPokemon().getStatStage(Stat.ATK)).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should bounce back a move before the accuracy check", async () => {
|
|
||||||
game.override.moveset([MoveId.SPORE]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const attacker = game.field.getPlayerPokemon();
|
|
||||||
|
|
||||||
vi.spyOn(attacker, "getAccuracyMultiplier").mockReturnValue(0.0);
|
|
||||||
game.move.select(MoveId.SPORE);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(game.field.getPlayerPokemon().status?.effect).toBe(StatusEffect.SLEEP);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should take the accuracy of the magic bounce user into account", async () => {
|
|
||||||
game.override.moveset([MoveId.SPORE]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
const opponent = game.field.getEnemyPokemon();
|
|
||||||
|
|
||||||
vi.spyOn(opponent, "getAccuracyMultiplier").mockReturnValue(0);
|
|
||||||
game.move.select(MoveId.SPORE);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(game.field.getPlayerPokemon().status).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
@ -371,9 +371,13 @@ export class GameManager {
|
|||||||
console.log("==================[New Turn]==================");
|
console.log("==================[New Turn]==================");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Transition to the {@linkcode TurnEndPhase | end of the current turn}. */
|
/**
|
||||||
async toEndOfTurn() {
|
* Transition to the {@linkcode TurnEndPhase | end of the current turn}.
|
||||||
await this.phaseInterceptor.to("TurnEndPhase");
|
* @param runTarget - Whether or not to run the {@linkcode TurnEndPhase}; default `true`
|
||||||
|
* @returns A Promise that resolves once the turn has ended.
|
||||||
|
*/
|
||||||
|
async toEndOfTurn(runTarget = true): Promise<void> {
|
||||||
|
await this.phaseInterceptor.to("TurnEndPhase", runTarget);
|
||||||
console.log("==================[End of Turn]==================");
|
console.log("==================[End of Turn]==================");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -536,14 +540,16 @@ export class GameManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modifies the queue manager to return move phases in a particular order
|
* Modifies the queue manager to return move phases in a particular order.
|
||||||
* Used to manually modify Pokemon turn order.
|
* Used to manually modify Pokemon turn order.
|
||||||
* Note: This *DOES NOT* account for priority.
|
*
|
||||||
* @param order - The turn order to set as an array of {@linkcode BattlerIndex}es.
|
* @param order - The turn order to set, as an array of {@linkcode BattlerIndex}es
|
||||||
* @example
|
* @example
|
||||||
* ```ts
|
* ```ts
|
||||||
* await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2]);
|
* await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2]);
|
||||||
* ```
|
* ```
|
||||||
|
* @remarks
|
||||||
|
* This *does not* account for priority and will override Trick Room's effect.
|
||||||
*/
|
*/
|
||||||
async setTurnOrder(order: BattlerIndex[]): Promise<void> {
|
async setTurnOrder(order: BattlerIndex[]): Promise<void> {
|
||||||
await this.phaseInterceptor.to("TurnStartPhase", false);
|
await this.phaseInterceptor.to("TurnStartPhase", false);
|
||||||
|
Loading…
Reference in New Issue
Block a user