mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-07-27 10:42:25 +02:00
Mostly working magic bounce and magic coat
This commit is contained in:
parent
9e8d4850a8
commit
a3db6d5552
@ -2352,14 +2352,14 @@ export default class BattleScene extends SceneBase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Phase to the end of phaseQueuePrepend, or at phaseQueuePrependSpliceIndex
|
||||
* @param phase {@linkcode Phase} the phase to add
|
||||
* Adds Phase(s) to the end of phaseQueuePrepend, or at phaseQueuePrependSpliceIndex
|
||||
* @param phases {@linkcode Phase} the phase(s) to add
|
||||
*/
|
||||
unshiftPhase(phase: Phase): void {
|
||||
unshiftPhase(...phases: Phase[]): void {
|
||||
if (this.phaseQueuePrependSpliceIndex === -1) {
|
||||
this.phaseQueuePrepend.push(phase);
|
||||
this.phaseQueuePrepend.push(...phases);
|
||||
} else {
|
||||
this.phaseQueuePrepend.splice(this.phaseQueuePrependSpliceIndex, 0, phase);
|
||||
this.phaseQueuePrepend.splice(this.phaseQueuePrependSpliceIndex, 0, ...phases);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2497,32 +2497,38 @@ export default class BattleScene extends SceneBase {
|
||||
* @param targetPhase {@linkcode Phase} the type of phase to search for in phaseQueue
|
||||
* @returns boolean if a targetPhase was found and added
|
||||
*/
|
||||
prependToPhase(phase: Phase, targetPhase: Constructor<Phase>): boolean {
|
||||
prependToPhase(phase: Phase | Phase [], targetPhase: Constructor<Phase>): boolean {
|
||||
if (!Array.isArray(phase)) {
|
||||
phase = [ phase ];
|
||||
}
|
||||
const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof targetPhase);
|
||||
|
||||
if (targetIndex !== -1) {
|
||||
this.phaseQueue.splice(targetIndex, 0, phase);
|
||||
this.phaseQueue.splice(targetIndex, 0, ...phase);
|
||||
return true;
|
||||
} else {
|
||||
this.unshiftPhase(phase);
|
||||
this.unshiftPhase(...phase);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to add the input phase to index after target phase in the {@linkcode phaseQueue}, else simply calls {@linkcode unshiftPhase()}
|
||||
* @param phase {@linkcode Phase} the phase to be added
|
||||
* Tries to add the input phase(s) to index after target phase in the {@linkcode phaseQueue}, else simply calls {@linkcode unshiftPhase()}
|
||||
* @param phase {@linkcode Phase} the phase(s) to be added
|
||||
* @param targetPhase {@linkcode Phase} the type of phase to search for in {@linkcode phaseQueue}
|
||||
* @returns `true` if a `targetPhase` was found to append to
|
||||
*/
|
||||
appendToPhase(phase: Phase, targetPhase: Constructor<Phase>): boolean {
|
||||
appendToPhase(phase: Phase | Phase[], targetPhase: Constructor<Phase>): boolean {
|
||||
if (!Array.isArray(phase)) {
|
||||
phase = [ phase ];
|
||||
}
|
||||
const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof targetPhase);
|
||||
|
||||
if (targetIndex !== -1 && this.phaseQueue.length > targetIndex) {
|
||||
this.phaseQueue.splice(targetIndex + 1, 0, phase);
|
||||
this.phaseQueue.splice(targetIndex + 1, 0, ...phase);
|
||||
return true;
|
||||
} else {
|
||||
this.unshiftPhase(phase);
|
||||
this.unshiftPhase(...phase);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -4493,6 +4493,13 @@ 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 Moves.MAGIC_COAT | Magic Coat}.
|
||||
*/
|
||||
export class ReflectStatusMoveAbAttr extends AbAttr { }
|
||||
|
||||
export class UncopiableAbilityAbAttr extends AbAttr {
|
||||
constructor() {
|
||||
super(false);
|
||||
@ -5809,7 +5816,11 @@ export function initAbilities() {
|
||||
}, Stat.SPD, 1)
|
||||
.attr(PostIntimidateStatStageChangeAbAttr, [ Stat.SPD ], 1),
|
||||
new Ability(Abilities.MAGIC_BOUNCE, 5)
|
||||
.attr(ReflectStatusMoveAbAttr)
|
||||
.ignorable()
|
||||
// Interactions with stomping tantrum, instruct, and other moves that
|
||||
// rely on move history
|
||||
.edgeCase()
|
||||
.unimplemented(),
|
||||
new Ability(Abilities.SAP_SIPPER, 5)
|
||||
.attr(TypeImmunityStatStageChangeAbAttr, Type.GRASS, Stat.ATK, 1)
|
||||
|
@ -5343,6 +5343,7 @@ export class AddBattlerTagAttr extends MoveEffectAttr {
|
||||
case BattlerTagType.INGRAIN:
|
||||
case BattlerTagType.IGNORE_ACCURACY:
|
||||
case BattlerTagType.AQUA_RING:
|
||||
case BattlerTagType.MAGIC_COAT:
|
||||
return 3;
|
||||
case BattlerTagType.PROTECTED:
|
||||
case BattlerTagType.FLYING:
|
||||
@ -9147,8 +9148,11 @@ export function initMoves() {
|
||||
new AttackMove(Moves.SUPERPOWER, Type.FIGHTING, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 3)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], -1, true),
|
||||
new SelfStatusMove(Moves.MAGIC_COAT, Type.PSYCHIC, -1, 15, -1, 4, 3)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.MAGIC_COAT, true, true)
|
||||
.unimplemented(),
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.MAGIC_COAT, true, false)
|
||||
// Interactions with stomping tantrum, instruct, and other moves that
|
||||
// rely on move history
|
||||
// Also will not reflect roar / whirlwind if the target has ForceSwitchOutImmunityAbAttr
|
||||
.edgeCase(),
|
||||
new SelfStatusMove(Moves.RECYCLE, Type.NORMAL, -1, 10, -1, 0, 3)
|
||||
.unimplemented(),
|
||||
new AttackMove(Moves.REVENGE, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 100, 10, -1, -4, 3)
|
||||
|
@ -94,5 +94,5 @@ export enum BattlerTagType {
|
||||
PSYCHO_SHIFT = "PSYCHO_SHIFT",
|
||||
ENDURE_TOKEN = "ENDURE_TOKEN",
|
||||
POWDER = "POWDER",
|
||||
MAGIC_COAT = "MAGIC_COAT"
|
||||
MAGIC_COAT = "MAGIC_COAT",
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
PostAttackAbAttr,
|
||||
PostDamageAbAttr,
|
||||
PostDefendAbAttr,
|
||||
ReflectStatusMoveAbAttr,
|
||||
TypeImmunityAbAttr,
|
||||
} from "#app/data/ability";
|
||||
import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag";
|
||||
@ -31,6 +32,7 @@ import {
|
||||
AttackMove,
|
||||
DelayedAttackAttr,
|
||||
FlinchAttr,
|
||||
getMoveTargets,
|
||||
HitsTagAttr,
|
||||
MissEffectAttr,
|
||||
MoveCategory,
|
||||
@ -47,7 +49,7 @@ import {
|
||||
} from "#app/data/move";
|
||||
import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms";
|
||||
import { Type } from "#enums/type";
|
||||
import type { PokemonMove } from "#app/field/pokemon";
|
||||
import { PokemonMove } from "#app/field/pokemon";
|
||||
import type Pokemon from "#app/field/pokemon";
|
||||
import { HitResult, MoveResult } from "#app/field/pokemon";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
@ -63,14 +65,25 @@ import { BooleanHolder, executeIf, isNullOrUndefined, NumberHolder } from "#app/
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import type { Moves } from "#enums/moves";
|
||||
import i18next from "i18next";
|
||||
import { Stat } from "#app/enums/stat";
|
||||
import { ArenaTagType } from "#app/enums/arena-tag-type";
|
||||
import { MessagePhase } from "./message-phase";
|
||||
import type { Phase } from "#app/phase";
|
||||
import { ShowAbilityPhase } from "./show-ability-phase";
|
||||
import { MovePhase } from "./move-phase";
|
||||
|
||||
export class MoveEffectPhase extends PokemonPhase {
|
||||
public move: PokemonMove;
|
||||
protected targets: BattlerIndex[];
|
||||
protected reflected: boolean = false;
|
||||
|
||||
constructor(battlerIndex: BattlerIndex, targets: BattlerIndex[], move: PokemonMove) {
|
||||
/**
|
||||
* @param reflected Indicates that the move was reflected by the user due to magic coat or magic bounce
|
||||
*/
|
||||
constructor(battlerIndex: BattlerIndex, targets: BattlerIndex[], move: PokemonMove, reflected: boolean = false) {
|
||||
super(battlerIndex);
|
||||
this.move = move;
|
||||
this.reflected = reflected;
|
||||
/**
|
||||
* In double battles, if the right Pokemon selects a spread move and the left Pokemon dies
|
||||
* with no party members available to switch in, then the right Pokemon takes the index
|
||||
@ -177,12 +190,14 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
&& (targets[0]?.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
|
||||
&& !targets[0]?.getTag(SemiInvulnerableTag);
|
||||
|
||||
const mayBounce = move.hasFlag(MoveFlags.REFLECTABLE) && !this.reflected;
|
||||
|
||||
/**
|
||||
* If no targets are left for the move to hit (FAIL), or the invoked move is single-target
|
||||
* If no targets are left for the move to hit (FAIL), or the invoked move is non-reflectable, single-target
|
||||
* (and not random target) and failed the hit check against its target (MISS), log the move
|
||||
* as FAILed or MISSed (depending on the conditions above) and end this phase.
|
||||
*/
|
||||
if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) {
|
||||
if (!hasActiveTargets || (mayBounce && !move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) {
|
||||
this.stopMultiHit();
|
||||
if (hasActiveTargets) {
|
||||
globalScene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getFirstTarget() ? getPokemonNameWithAffix(this.getFirstTarget()!) : "" }));
|
||||
@ -204,12 +219,30 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
new MoveAnim(move.id as Moves, user, this.getFirstTarget()!.getBattlerIndex(), playOnEmptyField).play(move.hitsSubstitute(user, this.getFirstTarget()!), () => {
|
||||
/** Has the move successfully hit a target (for damage) yet? */
|
||||
let hasHit: boolean = false;
|
||||
for (const target of targets) {
|
||||
// Prevent ENEMY_SIDE targeted moves from occurring twice in double battles
|
||||
if (move.moveTarget === MoveTarget.ENEMY_SIDE && target !== targets[targets.length - 1]) {
|
||||
continue;
|
||||
|
||||
// Prevent ENEMY_SIDE targeted moves from occurring twice in double battles
|
||||
// and determine which enemy will magic bounce based on speed order, respecting trick room
|
||||
const trueTargets: Pokemon[] = move.moveTarget !== MoveTarget.ENEMY_SIDE ? targets : (() => {
|
||||
const magicCoatTargets = targets.filter(t => t.getTag(BattlerTagType.MAGIC_COAT) !== null);
|
||||
|
||||
// only magic coat effect cares about order
|
||||
if (!mayBounce || magicCoatTargets.length === 0) {
|
||||
return [ targets[0] ];
|
||||
}
|
||||
|
||||
const reversed = globalScene.arena.hasTag(ArenaTagType.TRICK_ROOM);
|
||||
const sortedTargets = targets
|
||||
.sort((a: Pokemon, b: Pokemon) => {
|
||||
const aSpeed = a?.getEffectiveStat(Stat.SPD) ?? 0;
|
||||
const bSpeed = b?.getEffectiveStat(Stat.SPD) ?? 0;
|
||||
return reversed ? aSpeed - bSpeed : bSpeed - aSpeed;
|
||||
})
|
||||
.filter(t => t.getTag(BattlerTagType.MAGIC_COAT) !== null);
|
||||
return sortedTargets.length === 1 ? sortedTargets : [ sortedTargets[globalScene.randBattleSeedInt(sortedTargets.length)] ];
|
||||
})();
|
||||
|
||||
const queuedPhases: Phase[] = [];
|
||||
for (const target of trueTargets) {
|
||||
/** 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)? */
|
||||
@ -231,13 +264,34 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
|| (this.move.getMove().category !== MoveCategory.STATUS
|
||||
&& target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType))));
|
||||
|
||||
/** Is the target hidden by the effects of its Commander ability? */
|
||||
const isCommanding = globalScene.currentBattle.double && target.getAlly()?.getTag(BattlerTagType.COMMANDED)?.getSourcePokemon() === target;
|
||||
|
||||
/** Is the target reflecting status moves from the magic coat move? */
|
||||
const isReflecting = target.getTag(BattlerTagType.MAGIC_COAT) !== null;
|
||||
|
||||
/** Is the target's magic bounce ability not ignored and able to reflect this move? */
|
||||
const canMagicBounce = !(isReflecting || move.checkFlag(MoveFlags.IGNORE_ABILITIES, user, target) && target.hasAbilityWithAttr(ReflectStatusMoveAbAttr));
|
||||
|
||||
/** Is the target protected and reflecting the effect */
|
||||
const willBounce = !isProtected && !this.reflected && !isCommanding && (isReflecting || canMagicBounce);
|
||||
|
||||
/** If the move will bounce, then queue the bounce and move on to the next target*/
|
||||
if (!target.switchOutStatus && willBounce) {
|
||||
const newTargets = move.isMultiTarget() ? getMoveTargets(target, move.id).targets : [ user.getBattlerIndex() ];
|
||||
if (!isReflecting && canMagicBounce) {
|
||||
queuedPhases.push(new ShowAbilityPhase(target.getBattlerIndex(), target.getPassiveAbility().hasAttr(ReflectStatusMoveAbAttr)));
|
||||
}
|
||||
queuedPhases.push(new MessagePhase(i18next.t("battle:magicCoatBounce", { pokemonNameWithAffix: getPokemonNameWithAffix(target) })));
|
||||
queuedPhases.push(new MovePhase(target, newTargets, new PokemonMove(move.id, 0, 0, true), true, true, true));
|
||||
|
||||
}
|
||||
|
||||
/** Is the pokemon immune due to an ablility, and also not in a semi invulnerable state? */
|
||||
const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr)
|
||||
&& (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
|
||||
&& !target.getTag(SemiInvulnerableTag);
|
||||
|
||||
/** Is the target hidden by the effects of its Commander ability? */
|
||||
const isCommanding = globalScene.currentBattle.double && target.getAlly()?.getTag(BattlerTagType.COMMANDED)?.getSourcePokemon() === target;
|
||||
|
||||
/**
|
||||
* If the move missed a target, stop all future hits against that target
|
||||
@ -364,6 +418,8 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
applyAttrs.push(k);
|
||||
}
|
||||
|
||||
// Apply queued phases
|
||||
queuedPhases.forEach(p => globalScene.unshiftPhase(p));
|
||||
// Apply the move's POST_TARGET effects on the move's last hit, after all targeted effects have resolved
|
||||
const postTarget = (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive()) ?
|
||||
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET, user, null, move) :
|
||||
|
@ -58,6 +58,7 @@ export class MovePhase extends BattlePhase {
|
||||
protected ignorePp: boolean;
|
||||
protected failed: boolean = false;
|
||||
protected cancelled: boolean = false;
|
||||
protected reflected: boolean = false;
|
||||
|
||||
public get pokemon(): Pokemon {
|
||||
return this._pokemon;
|
||||
@ -84,10 +85,12 @@ export class MovePhase extends BattlePhase {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param followUp Indicates that the move being uses is a "follow-up" - for example, a move being used by Metronome or Dancer.
|
||||
* @param followUp Indicates that the move being used is a "follow-up" - for example, a move being used by Metronome or Dancer.
|
||||
* Follow-ups bypass a few failure conditions, including flinches, sleep/paralysis/freeze and volatile status checks, etc.
|
||||
* @param reflected Indicates that the move was reflected by Magic Coat or Magic Bounce.
|
||||
* Reflected moves cannot be reflected again and will not trigger Dancer.
|
||||
*/
|
||||
constructor(pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, followUp: boolean = false, ignorePp: boolean = false) {
|
||||
constructor(pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, followUp: boolean = false, ignorePp: boolean = false, reflected: boolean = false) {
|
||||
super();
|
||||
|
||||
this.pokemon = pokemon;
|
||||
@ -95,6 +98,7 @@ export class MovePhase extends BattlePhase {
|
||||
this.move = move;
|
||||
this.followUp = followUp;
|
||||
this.ignorePp = ignorePp;
|
||||
this.reflected = reflected;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -140,7 +144,7 @@ export class MovePhase extends BattlePhase {
|
||||
}
|
||||
|
||||
// Check move to see if arena.ignoreAbilities should be true.
|
||||
if (!this.followUp) {
|
||||
if (!this.followUp || this.reflected) {
|
||||
if (this.move.getMove().checkFlag(MoveFlags.IGNORE_ABILITIES, this.pokemon, null)) {
|
||||
globalScene.arena.setIgnoreAbilities(true, this.pokemon.getBattlerIndex());
|
||||
}
|
||||
@ -335,7 +339,7 @@ export class MovePhase extends BattlePhase {
|
||||
*/
|
||||
if (success) {
|
||||
applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, this.move.getMove());
|
||||
globalScene.unshiftPhase(new MoveEffectPhase(this.pokemon.getBattlerIndex(), this.targets, this.move));
|
||||
globalScene.unshiftPhase(new MoveEffectPhase(this.pokemon.getBattlerIndex(), this.targets, this.move, this.reflected));
|
||||
|
||||
} else {
|
||||
if ([ Moves.ROAR, Moves.WHIRLWIND, Moves.TRICK_OR_TREAT, Moves.FORESTS_CURSE ].includes(this.move.moveId)) {
|
||||
|
@ -148,22 +148,6 @@ describe("Abilities - Magic Bounce", () => {
|
||||
expect(game.scene.getEnemyPokemon()!.getTag(BattlerTagType.CURSED)).toBeDefined();
|
||||
});
|
||||
|
||||
// todo: a move reflected by magic bounce counts as though it failed.
|
||||
|
||||
it("should not count the bounced move as the last move used", async () => {
|
||||
game.override.enemyMoveset([ Moves.INSTRUCT, Moves.GROWL, Moves.SPLASH ]);
|
||||
game.override.battleType("double");
|
||||
await game.classicMode.startBattle([ Species.MAGIKARP, Species.MAGIKARP ]);
|
||||
|
||||
game.move.select(Moves.GROWL, 0);
|
||||
game.move.select(Moves.SPLASH, 1);
|
||||
game.forceEnemyMove(Moves.SPLASH);
|
||||
game.forceEnemyMove(Moves.INSTRUCT);
|
||||
game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2 ]);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
expect(game.scene.getEnemyPokemon()!.getLastXMoves(1)[0].move).toEqual([ Moves.SPLASH ]);
|
||||
});
|
||||
|
||||
it("should cause stomping tantrum to double in power if the bounced move fails", async () => {
|
||||
game.override.moveset([ Moves.SPLASH ]);
|
||||
await game.classicMode.startBattle([ Species.MAGIKARP ]);
|
||||
|
Loading…
Reference in New Issue
Block a user