mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-14 11:29:28 +02:00
Moved reflection check out of MEP and into ability side logic
This commit is contained in:
parent
cbc6f6b89e
commit
b2d10b7006
@ -1,6 +1,8 @@
|
||||
/* biome-ignore-start lint/correctness/noUnusedImports: tsdoc imports */
|
||||
import type { BattleScene } from "#app/battle-scene";
|
||||
import type { SpeciesFormChangeRevertWeatherFormTrigger } from "#data/form-change-triggers";
|
||||
import type { MoveEffectPhase } from "#phases/move-effect-phase";
|
||||
import type { MoveEndPhase } from "#phases/move-end-phase";
|
||||
/* biome-ignore-end lint/correctness/noUnusedImports: tsdoc imports */
|
||||
|
||||
import { applyAbAttrs } from "#abilities/apply-ab-attrs";
|
||||
@ -51,7 +53,8 @@ import { BerryModifierType } from "#modifiers/modifier-type";
|
||||
import { applyMoveAttrs } from "#moves/apply-attrs";
|
||||
import { noAbilityTypeOverrideMoves } from "#moves/invalid-moves";
|
||||
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 {
|
||||
AbAttrCondition,
|
||||
@ -5788,12 +5791,21 @@ export class InfiltratorAbAttr extends AbAttr {
|
||||
/**
|
||||
* 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}
|
||||
* moves as if the user had used {@linkcode MoveId.MAGIC_COAT | Magic Coat}.
|
||||
* @sealed
|
||||
* @todo Make reflection a part of this ability's effects
|
||||
* moves as if the user had used {@linkcode MoveId.MAGIC_COAT | Magic Coat}.
|
||||
* The calling {@linkcode MoveEffectPhase} will "skip" targets with a reflection effect active,
|
||||
* showing the flyout and queueing the reaction during the move's {@linkcode MoveEndPhase}.
|
||||
*/
|
||||
export class ReflectStatusMoveAbAttr extends AbAttr {
|
||||
private declare readonly _: never;
|
||||
export class ReflectStatusMoveAbAttr extends PreDefendAbAttr {
|
||||
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
|
||||
@ -7250,10 +7262,7 @@ export function initAbilities() {
|
||||
.attr(PostIntimidateStatStageChangeAbAttr, [ Stat.SPD ], 1),
|
||||
new Ability(AbilityId.MAGIC_BOUNCE, 5)
|
||||
.attr(ReflectStatusMoveAbAttr)
|
||||
.ignorable()
|
||||
// Interactions with stomping tantrum, instruct, encore, and probably other moves that
|
||||
// rely on move history
|
||||
.edgeCase(),
|
||||
.ignorable(),
|
||||
new Ability(AbilityId.SAP_SIPPER, 5)
|
||||
.attr(TypeImmunityStatStageChangeAbAttr, PokemonType.GRASS, Stat.ATK, 1)
|
||||
.ignorable(),
|
||||
@ -7351,7 +7360,7 @@ export function initAbilities() {
|
||||
new Ability(AbilityId.GOOEY, 6)
|
||||
.attr(PostDefendStatStageChangeAbAttr, (_target, _user, move) => move.hasFlag(MoveFlags.MAKES_CONTACT), Stat.SPD, -1, false),
|
||||
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)
|
||||
.attr(AddSecondStrikeAbAttr, 0.25),
|
||||
new Ability(AbilityId.DARK_AURA, 6)
|
||||
|
@ -29,6 +29,7 @@ import { applyMoveAttrs } from "#moves/apply-attrs";
|
||||
import { invalidEncoreMoves } from "#moves/invalid-moves";
|
||||
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 { MovePhase } from "#phases/move-phase";
|
||||
import type { StatStageChangeCallback } from "#phases/stat-stage-change-phase";
|
||||
@ -175,6 +176,7 @@ export class BattlerTag implements BaseBattlerTag {
|
||||
return "";
|
||||
}
|
||||
|
||||
// TODO: Make this a getter
|
||||
isSourceLinked(): boolean {
|
||||
return false;
|
||||
}
|
||||
@ -3640,6 +3642,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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,8 +1,13 @@
|
||||
import { SemiInvulnerableTag } from "#data/battler-tags";
|
||||
import { allMoves } from "#data/data-lists";
|
||||
// biome-ignore lint/correctness/noUnusedImports: <explanation>
|
||||
import type { AbilityId } from "#enums/ability-id";
|
||||
import type { BattlerIndex } from "#enums/battler-index";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { MoveFlags } from "#enums/move-flags";
|
||||
import type { MoveId } from "#enums/move-id";
|
||||
import { MoveTarget } from "#enums/move-target";
|
||||
import { isReflected, type MoveUseMode } from "#enums/move-use-mode";
|
||||
import { PokemonType } from "#enums/pokemon-type";
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
import { applyMoveAttrs } from "#moves/apply-attrs";
|
||||
@ -133,3 +138,24 @@ export const frenzyMissFunc: UserMoveConditionFunc = (user: Pokemon, move: Move)
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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}.
|
||||
*/
|
||||
export 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"))
|
||||
);
|
||||
}
|
||||
|
@ -674,20 +674,9 @@ export abstract class Move implements Localizable {
|
||||
return true;
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -44,6 +44,7 @@ import { MoveEffectPhase } from "#phases/move-effect-phase";
|
||||
import { MoveEndPhase } from "#phases/move-end-phase";
|
||||
import { MoveHeaderPhase } from "#phases/move-header-phase";
|
||||
import { MovePhase } from "#phases/move-phase";
|
||||
import { MoveReflectPhase } from "#phases/move-reflect-phase";
|
||||
import {
|
||||
MysteryEncounterBattlePhase,
|
||||
MysteryEncounterBattleStartCleanupPhase,
|
||||
@ -157,6 +158,7 @@ const PHASES = Object.freeze({
|
||||
MoveEffectPhase,
|
||||
MoveEndPhase,
|
||||
MoveHeaderPhase,
|
||||
MoveReflectPhase,
|
||||
MovePhase,
|
||||
MysteryEncounterPhase,
|
||||
MysteryEncounterOptionSelectedPhase,
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { applyAbAttrs } from "#abilities/apply-ab-attrs";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import type { Phase } from "#app/phase";
|
||||
import { ConditionalProtectTag } from "#data/arena-tag";
|
||||
import { MoveAnim } from "#data/battle-anims";
|
||||
import { DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag, TypeBoostTag } from "#data/battler-tags";
|
||||
@ -19,7 +18,7 @@ import { MoveFlags } from "#enums/move-flags";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { MoveTarget } from "#enums/move-target";
|
||||
import { isReflected, MoveUseMode } from "#enums/move-use-mode";
|
||||
import { MoveUseMode } from "#enums/move-use-mode";
|
||||
import { PokemonType } from "#enums/pokemon-type";
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
import {
|
||||
@ -33,8 +32,7 @@ import {
|
||||
} from "#modifiers/modifier";
|
||||
import { applyFilteredMoveAttrs, applyMoveAttrs } from "#moves/apply-attrs";
|
||||
import type { Move, MoveAttr } from "#moves/move";
|
||||
import { getMoveTargets, isFieldTargeted } from "#moves/move-utils";
|
||||
import { PokemonMove } from "#moves/pokemon-move";
|
||||
import { isFieldTargeted, isMoveReflectableBy } from "#moves/move-utils";
|
||||
import { PokemonPhase } from "#phases/pokemon-phase";
|
||||
import { DamageAchv } from "#system/achv";
|
||||
import type { DamageResult } from "#types/damage-result";
|
||||
@ -67,12 +65,6 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
/** Is this the last strike of a move? */
|
||||
private lastHit: boolean;
|
||||
|
||||
/**
|
||||
* Phases queued during moves; used to add a new MovePhase for reflected moves after triggering.
|
||||
* TODO: Remove this and move the reflection logic to ability-side
|
||||
*/
|
||||
private queuedPhases: Phase[] = [];
|
||||
|
||||
/**
|
||||
* @param useMode - The {@linkcode MoveUseMode} corresponding to how this move was used.
|
||||
*/
|
||||
@ -147,39 +139,6 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
return targets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue the phaes 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)) {
|
||||
this.queuedPhases.push(
|
||||
globalScene.phaseManager.create(
|
||||
"ShowAbilityPhase",
|
||||
target.getBattlerIndex(),
|
||||
target.getPassiveAbility().hasAttr("ReflectStatusMoveAbAttr"),
|
||||
),
|
||||
);
|
||||
this.queuedPhases.push(globalScene.phaseManager.create("HideAbilityPhase"));
|
||||
}
|
||||
|
||||
this.queuedPhases.push(
|
||||
globalScene.phaseManager.create(
|
||||
"MovePhase",
|
||||
target,
|
||||
newTargets,
|
||||
new PokemonMove(this.move.id),
|
||||
MoveUseMode.REFLECTED,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the move to each of the resolved targets.
|
||||
* @param targets - The resolved set of targets of the move
|
||||
@ -217,7 +176,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
applyMoveAttrs("MissEffectAttr", user, target, this.move);
|
||||
break;
|
||||
case HitCheckResult.REFLECTED:
|
||||
this.queueReflectedMove(user, target);
|
||||
globalScene.phaseManager.appendNewToPhase("MoveEndPhase", "MoveReflectPhase", target, user, this.move);
|
||||
break;
|
||||
case HitCheckResult.PENDING:
|
||||
case HitCheckResult.ERROR:
|
||||
@ -344,9 +303,6 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.queuedPhases.length) {
|
||||
globalScene.phaseManager.appendToPhase(this.queuedPhases, "MoveEndPhase");
|
||||
}
|
||||
const moveType = user.getMoveType(this.move, true);
|
||||
if (this.move.category !== MoveCategory.STATUS && !user.stellarTypesBoosted.includes(moveType)) {
|
||||
user.stellarTypesBoosted.push(moveType);
|
||||
@ -531,8 +487,8 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
return [HitCheckResult.PROTECTED, 0];
|
||||
}
|
||||
|
||||
// Reflected moves cannot be reflected again
|
||||
if (!isReflected(this.useMode) && move.doesFlagEffectApply({ flag: MoveFlags.REFLECTABLE, user, target })) {
|
||||
// Check for magic bounce
|
||||
if (isMoveReflectableBy(move, target, this.useMode)) {
|
||||
return [HitCheckResult.REFLECTED, 0];
|
||||
}
|
||||
|
||||
|
40
src/phases/move-reflect-phase.ts
Normal file
40
src/phases/move-reflect-phase.ts
Normal file
@ -0,0 +1,40 @@
|
||||
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 {
|
||||
// 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();
|
||||
}
|
||||
}
|
@ -10,7 +10,6 @@ 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 type { EnemyPokemon } from "#field/pokemon";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
@ -70,7 +69,7 @@ describe("Moves - Reflecting effects", () => {
|
||||
expect(karp2).toHaveStatStage(Stat.ATK, -2);
|
||||
});
|
||||
|
||||
// TODO: This is broken...
|
||||
// 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]);
|
||||
@ -144,12 +143,13 @@ describe("Moves - Reflecting effects", () => {
|
||||
});
|
||||
|
||||
it("should not cause the bounced move to count for encore", async () => {
|
||||
game.override.battleStyle("double").enemyAbility(AbilityId.MAGIC_BOUNCE);
|
||||
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, enemy2] = game.scene.getField();
|
||||
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 });
|
||||
|
||||
@ -157,17 +157,18 @@ describe("Moves - Reflecting effects", () => {
|
||||
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.killPokemon(enemy2 as EnemyPokemon);
|
||||
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]);
|
||||
await game.toEndOfTurn();
|
||||
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.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 () => {
|
||||
await game.override.ability(AbilityId.INSOMNIA);
|
||||
game.override.ability(AbilityId.INSOMNIA);
|
||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
|
Loading…
Reference in New Issue
Block a user