[Ability] [Move] Implement Magic Bounce and Magic Coat (#5225)

* Add unit tests for magic bounce

* Add reflectable tag and apply to moves

* Add BattlerTagType for Magic Coat

* Add more magic bounce tests

* Add magic bounce test for sticky web source

* Mostly working magic bounce and magic coat

* Fix missing negation on mayBounce check

* Move onto the next target after bouncing

* Fix magic bounce accuracy check test

* Finish magic bounce impl

* Make spikes use leftmost magic bounce target

* Add magic coat tests

* Add MagicCoatTag to battler-tags.ts

* Add final set of tests for Magic Coat / Bounce

* Fix semi invulnerbale check in hitCheck

* Fix magic bounce semi-invulnerable interaction

This was based on smogon's incorrect handling of this situation

* Magic bounce should not bounce anything during semi-invulnerable state

* Activate mirror armor interaction test

Also update i18 locales key to `magicCoatActivated`
This commit is contained in:
Sirz Benjie 2025-02-11 05:25:36 -06:00 committed by GitHub
parent 702a6ba482
commit 5296966f70
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 1005 additions and 131 deletions

View File

@ -2353,14 +2353,14 @@ export default class BattleScene extends SceneBase {
} }
/** /**
* Adds Phase to the end of phaseQueuePrepend, or at phaseQueuePrependSpliceIndex * Adds Phase(s) to the end of phaseQueuePrepend, or at phaseQueuePrependSpliceIndex
* @param phase {@linkcode Phase} the phase to add * @param phases {@linkcode Phase} the phase(s) to add
*/ */
unshiftPhase(phase: Phase): void { unshiftPhase(...phases: Phase[]): void {
if (this.phaseQueuePrependSpliceIndex === -1) { if (this.phaseQueuePrependSpliceIndex === -1) {
this.phaseQueuePrepend.push(phase); this.phaseQueuePrepend.push(...phases);
} else { } else {
this.phaseQueuePrepend.splice(this.phaseQueuePrependSpliceIndex, 0, phase); this.phaseQueuePrepend.splice(this.phaseQueuePrependSpliceIndex, 0, ...phases);
} }
} }
@ -2498,32 +2498,38 @@ export default class BattleScene extends SceneBase {
* @param targetPhase {@linkcode Phase} the type of phase to search for in phaseQueue * @param targetPhase {@linkcode Phase} the type of phase to search for in phaseQueue
* @returns boolean if a targetPhase was found and added * @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); const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof targetPhase);
if (targetIndex !== -1) { if (targetIndex !== -1) {
this.phaseQueue.splice(targetIndex, 0, phase); this.phaseQueue.splice(targetIndex, 0, ...phase);
return true; return true;
} else { } else {
this.unshiftPhase(phase); this.unshiftPhase(...phase);
return false; return false;
} }
} }
/** /**
* Tries to add the input phase to index after target phase in the {@linkcode phaseQueue}, else simply calls {@linkcode unshiftPhase()} * 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 to be added * @param phase {@linkcode Phase} the phase(s) to be added
* @param targetPhase {@linkcode Phase} the type of phase to search for in {@linkcode phaseQueue} * @param targetPhase {@linkcode Phase} the type of phase to search for in {@linkcode phaseQueue}
* @returns `true` if a `targetPhase` was found to append to * @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); const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof targetPhase);
if (targetIndex !== -1 && this.phaseQueue.length > targetIndex) { if (targetIndex !== -1 && this.phaseQueue.length > targetIndex) {
this.phaseQueue.splice(targetIndex + 1, 0, phase); this.phaseQueue.splice(targetIndex + 1, 0, ...phase);
return true; return true;
} else { } else {
this.unshiftPhase(phase); this.unshiftPhase(...phase);
return false; return false;
} }
} }

View File

@ -4484,6 +4484,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 { export class UncopiableAbilityAbAttr extends AbAttr {
constructor() { constructor() {
super(false); super(false);
@ -5805,8 +5812,11 @@ export function initAbilities() {
}, Stat.SPD, 1) }, Stat.SPD, 1)
.attr(PostIntimidateStatStageChangeAbAttr, [ Stat.SPD ], 1), .attr(PostIntimidateStatStageChangeAbAttr, [ Stat.SPD ], 1),
new Ability(Abilities.MAGIC_BOUNCE, 5) new Ability(Abilities.MAGIC_BOUNCE, 5)
.attr(ReflectStatusMoveAbAttr)
.ignorable() .ignorable()
.unimplemented(), // Interactions with stomping tantrum, instruct, encore, and probably other moves that
// rely on move history
.edgeCase(),
new Ability(Abilities.SAP_SIPPER, 5) new Ability(Abilities.SAP_SIPPER, 5)
.attr(TypeImmunityStatStageChangeAbAttr, Type.GRASS, Stat.ATK, 1) .attr(TypeImmunityStatStageChangeAbAttr, Type.GRASS, Stat.ATK, 1)
.ignorable(), .ignorable(),

View File

@ -2975,6 +2975,24 @@ export class PsychoShiftTag extends BattlerTag {
} }
} }
/**
* Tag associated with the move Magic Coat.
*/
export class MagicCoatTag extends BattlerTag {
constructor() {
super(BattlerTagType.MAGIC_COAT, BattlerTagLapseType.TURN_END, 1, Moves.MAGIC_COAT);
}
/**
* Queues the "[PokemonName] shrouded itself with Magic Coat" message when the tag is added.
* @param pokemon - The target {@linkcode Pokemon}
*/
override onAdd(pokemon: Pokemon) {
// "{pokemonNameWithAffix} shrouded itself with Magic Coat!"
globalScene.queueMessage(i18next.t("battlerTags:magicCoatOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
}
}
/** /**
* Retrieves a {@linkcode BattlerTag} based on the provided tag type, turn count, source move, and source ID. * Retrieves a {@linkcode BattlerTag} based on the provided tag type, turn count, source move, and source ID.
* @param sourceId - The ID of the pokemon adding the tag * @param sourceId - The ID of the pokemon adding the tag
@ -3164,6 +3182,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
return new GrudgeTag(); return new GrudgeTag();
case BattlerTagType.PSYCHO_SHIFT: case BattlerTagType.PSYCHO_SHIFT:
return new PsychoShiftTag(); return new PsychoShiftTag();
case BattlerTagType.MAGIC_COAT:
return new MagicCoatTag();
case BattlerTagType.NONE: case BattlerTagType.NONE:
default: default:
return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId); return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);

View File

@ -126,6 +126,8 @@ export enum MoveFlags {
IGNORE_SUBSTITUTE = 1 << 17, IGNORE_SUBSTITUTE = 1 << 17,
/** Indicates a move is able to be redirected to allies in a double battle if the attacker faints */ /** Indicates a move is able to be redirected to allies in a double battle if the attacker faints */
REDIRECT_COUNTER = 1 << 18, REDIRECT_COUNTER = 1 << 18,
/** Indicates a move is able to be reflected by {@linkcode Abilities.MAGIC_BOUNCE} and {@linkcode Moves.MAGIC_COAT} */
REFLECTABLE = 1 << 19,
} }
type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean; type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean;
@ -610,6 +612,16 @@ export default class Move implements Localizable {
return this; return this;
} }
/**
* Sets the {@linkcode MoveFlags.REFLECTABLE} flag for the calling Move
* @see {@linkcode Moves.ATTRACT}
* @returns The {@linkcode Move} that called this function
*/
reflectable(): this {
this.setFlag(MoveFlags.REFLECTABLE, true);
return this;
}
/** /**
* Checks if the move flag applies to the pokemon(s) using/receiving the move * Checks if the move flag applies to the pokemon(s) using/receiving the move
* @param flag {@linkcode MoveFlags} MoveFlag to check on user and/or target * @param flag {@linkcode MoveFlags} MoveFlag to check on user and/or target
@ -5332,6 +5344,7 @@ export class AddBattlerTagAttr extends MoveEffectAttr {
case BattlerTagType.INGRAIN: case BattlerTagType.INGRAIN:
case BattlerTagType.IGNORE_ACCURACY: case BattlerTagType.IGNORE_ACCURACY:
case BattlerTagType.AQUA_RING: case BattlerTagType.AQUA_RING:
case BattlerTagType.MAGIC_COAT:
return 3; return 3;
case BattlerTagType.PROTECTED: case BattlerTagType.PROTECTED:
case BattlerTagType.FLYING: case BattlerTagType.FLYING:
@ -8334,7 +8347,8 @@ export function initMoves() {
.attr(ForceSwitchOutAttr, false, SwitchType.FORCE_SWITCH) .attr(ForceSwitchOutAttr, false, SwitchType.FORCE_SWITCH)
.ignoresSubstitute() .ignoresSubstitute()
.hidesTarget() .hidesTarget()
.windMove(), .windMove()
.reflectable(),
new ChargingAttackMove(Moves.FLY, Type.FLYING, MoveCategory.PHYSICAL, 90, 95, 15, -1, 0, 1) new ChargingAttackMove(Moves.FLY, Type.FLYING, MoveCategory.PHYSICAL, 90, 95, 15, -1, 0, 1)
.chargeText(i18next.t("moveTriggers:flewUpHigh", { pokemonName: "{USER}" })) .chargeText(i18next.t("moveTriggers:flewUpHigh", { pokemonName: "{USER}" }))
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING) .chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING)
@ -8358,7 +8372,8 @@ export function initMoves() {
new AttackMove(Moves.ROLLING_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 85, 15, 30, 0, 1) new AttackMove(Moves.ROLLING_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 85, 15, 30, 0, 1)
.attr(FlinchAttr), .attr(FlinchAttr),
new StatusMove(Moves.SAND_ATTACK, Type.GROUND, 100, 15, -1, 0, 1) new StatusMove(Moves.SAND_ATTACK, Type.GROUND, 100, 15, -1, 0, 1)
.attr(StatStageChangeAttr, [ Stat.ACC ], -1), .attr(StatStageChangeAttr, [ Stat.ACC ], -1)
.reflectable(),
new AttackMove(Moves.HEADBUTT, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 15, 30, 0, 1) new AttackMove(Moves.HEADBUTT, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 15, 30, 0, 1)
.attr(FlinchAttr), .attr(FlinchAttr),
new AttackMove(Moves.HORN_ATTACK, Type.NORMAL, MoveCategory.PHYSICAL, 65, 100, 25, -1, 0, 1), new AttackMove(Moves.HORN_ATTACK, Type.NORMAL, MoveCategory.PHYSICAL, 65, 100, 25, -1, 0, 1),
@ -8387,7 +8402,8 @@ export function initMoves() {
.recklessMove(), .recklessMove(),
new StatusMove(Moves.TAIL_WHIP, Type.NORMAL, 100, 30, -1, 0, 1) new StatusMove(Moves.TAIL_WHIP, Type.NORMAL, 100, 30, -1, 0, 1)
.attr(StatStageChangeAttr, [ Stat.DEF ], -1) .attr(StatStageChangeAttr, [ Stat.DEF ], -1)
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES)
.reflectable(),
new AttackMove(Moves.POISON_STING, Type.POISON, MoveCategory.PHYSICAL, 15, 100, 35, 30, 0, 1) new AttackMove(Moves.POISON_STING, Type.POISON, MoveCategory.PHYSICAL, 15, 100, 35, 30, 0, 1)
.attr(StatusEffectAttr, StatusEffect.POISON) .attr(StatusEffectAttr, StatusEffect.POISON)
.makesContact(false), .makesContact(false),
@ -8400,30 +8416,36 @@ export function initMoves() {
.makesContact(false), .makesContact(false),
new StatusMove(Moves.LEER, Type.NORMAL, 100, 30, -1, 0, 1) new StatusMove(Moves.LEER, Type.NORMAL, 100, 30, -1, 0, 1)
.attr(StatStageChangeAttr, [ Stat.DEF ], -1) .attr(StatStageChangeAttr, [ Stat.DEF ], -1)
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES)
.reflectable(),
new AttackMove(Moves.BITE, Type.DARK, MoveCategory.PHYSICAL, 60, 100, 25, 30, 0, 1) new AttackMove(Moves.BITE, Type.DARK, MoveCategory.PHYSICAL, 60, 100, 25, 30, 0, 1)
.attr(FlinchAttr) .attr(FlinchAttr)
.bitingMove(), .bitingMove(),
new StatusMove(Moves.GROWL, Type.NORMAL, 100, 40, -1, 0, 1) new StatusMove(Moves.GROWL, Type.NORMAL, 100, 40, -1, 0, 1)
.attr(StatStageChangeAttr, [ Stat.ATK ], -1) .attr(StatStageChangeAttr, [ Stat.ATK ], -1)
.soundBased() .soundBased()
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES)
.reflectable(),
new StatusMove(Moves.ROAR, Type.NORMAL, -1, 20, -1, -6, 1) new StatusMove(Moves.ROAR, Type.NORMAL, -1, 20, -1, -6, 1)
.attr(ForceSwitchOutAttr, false, SwitchType.FORCE_SWITCH) .attr(ForceSwitchOutAttr, false, SwitchType.FORCE_SWITCH)
.soundBased() .soundBased()
.hidesTarget(), .hidesTarget()
.reflectable(),
new StatusMove(Moves.SING, Type.NORMAL, 55, 15, -1, 0, 1) new StatusMove(Moves.SING, Type.NORMAL, 55, 15, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.SLEEP) .attr(StatusEffectAttr, StatusEffect.SLEEP)
.soundBased(), .soundBased()
.reflectable(),
new StatusMove(Moves.SUPERSONIC, Type.NORMAL, 55, 20, -1, 0, 1) new StatusMove(Moves.SUPERSONIC, Type.NORMAL, 55, 20, -1, 0, 1)
.attr(ConfuseAttr) .attr(ConfuseAttr)
.soundBased(), .soundBased()
.reflectable(),
new AttackMove(Moves.SONIC_BOOM, Type.NORMAL, MoveCategory.SPECIAL, -1, 90, 20, -1, 0, 1) new AttackMove(Moves.SONIC_BOOM, Type.NORMAL, MoveCategory.SPECIAL, -1, 90, 20, -1, 0, 1)
.attr(FixedDamageAttr, 20), .attr(FixedDamageAttr, 20),
new StatusMove(Moves.DISABLE, Type.NORMAL, 100, 20, -1, 0, 1) new StatusMove(Moves.DISABLE, Type.NORMAL, 100, 20, -1, 0, 1)
.attr(AddBattlerTagAttr, BattlerTagType.DISABLED, false, true) .attr(AddBattlerTagAttr, BattlerTagType.DISABLED, false, true)
.condition((user, target, move) => target.getMoveHistory().reverse().find(m => m.move !== Moves.NONE && m.move !== Moves.STRUGGLE && !m.virtual) !== undefined) .condition((user, target, move) => target.getMoveHistory().reverse().find(m => m.move !== Moves.NONE && m.move !== Moves.STRUGGLE && !m.virtual) !== undefined)
.ignoresSubstitute(), .ignoresSubstitute()
.reflectable(),
new AttackMove(Moves.ACID, Type.POISON, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1) new AttackMove(Moves.ACID, Type.POISON, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1)
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -1) .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1)
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
@ -8476,7 +8498,8 @@ export function initMoves() {
.triageMove(), .triageMove(),
new StatusMove(Moves.LEECH_SEED, Type.GRASS, 90, 10, -1, 0, 1) new StatusMove(Moves.LEECH_SEED, Type.GRASS, 90, 10, -1, 0, 1)
.attr(LeechSeedAttr) .attr(LeechSeedAttr)
.condition((user, target, move) => !target.getTag(BattlerTagType.SEEDED) && !target.isOfType(Type.GRASS)), .condition((user, target, move) => !target.getTag(BattlerTagType.SEEDED) && !target.isOfType(Type.GRASS))
.reflectable(),
new SelfStatusMove(Moves.GROWTH, Type.NORMAL, -1, 20, -1, 0, 1) new SelfStatusMove(Moves.GROWTH, Type.NORMAL, -1, 20, -1, 0, 1)
.attr(GrowthStatStageChangeAttr), .attr(GrowthStatStageChangeAttr),
new AttackMove(Moves.RAZOR_LEAF, Type.GRASS, MoveCategory.PHYSICAL, 55, 95, 25, -1, 0, 1) new AttackMove(Moves.RAZOR_LEAF, Type.GRASS, MoveCategory.PHYSICAL, 55, 95, 25, -1, 0, 1)
@ -8490,13 +8513,16 @@ export function initMoves() {
.attr(AntiSunlightPowerDecreaseAttr), .attr(AntiSunlightPowerDecreaseAttr),
new StatusMove(Moves.POISON_POWDER, Type.POISON, 75, 35, -1, 0, 1) new StatusMove(Moves.POISON_POWDER, Type.POISON, 75, 35, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.POISON) .attr(StatusEffectAttr, StatusEffect.POISON)
.powderMove(), .powderMove()
.reflectable(),
new StatusMove(Moves.STUN_SPORE, Type.GRASS, 75, 30, -1, 0, 1) new StatusMove(Moves.STUN_SPORE, Type.GRASS, 75, 30, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.PARALYSIS) .attr(StatusEffectAttr, StatusEffect.PARALYSIS)
.powderMove(), .powderMove()
.reflectable(),
new StatusMove(Moves.SLEEP_POWDER, Type.GRASS, 75, 15, -1, 0, 1) new StatusMove(Moves.SLEEP_POWDER, Type.GRASS, 75, 15, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.SLEEP) .attr(StatusEffectAttr, StatusEffect.SLEEP)
.powderMove(), .powderMove()
.reflectable(),
new AttackMove(Moves.PETAL_DANCE, Type.GRASS, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 1) new AttackMove(Moves.PETAL_DANCE, Type.GRASS, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 1)
.attr(FrenzyAttr) .attr(FrenzyAttr)
.attr(MissEffectAttr, frenzyMissFunc) .attr(MissEffectAttr, frenzyMissFunc)
@ -8506,7 +8532,8 @@ export function initMoves() {
.target(MoveTarget.RANDOM_NEAR_ENEMY), .target(MoveTarget.RANDOM_NEAR_ENEMY),
new StatusMove(Moves.STRING_SHOT, Type.BUG, 95, 40, -1, 0, 1) new StatusMove(Moves.STRING_SHOT, Type.BUG, 95, 40, -1, 0, 1)
.attr(StatStageChangeAttr, [ Stat.SPD ], -2) .attr(StatStageChangeAttr, [ Stat.SPD ], -2)
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES)
.reflectable(),
new AttackMove(Moves.DRAGON_RAGE, Type.DRAGON, MoveCategory.SPECIAL, -1, 100, 10, -1, 0, 1) new AttackMove(Moves.DRAGON_RAGE, Type.DRAGON, MoveCategory.SPECIAL, -1, 100, 10, -1, 0, 1)
.attr(FixedDamageAttr, 40), .attr(FixedDamageAttr, 40),
new AttackMove(Moves.FIRE_SPIN, Type.FIRE, MoveCategory.SPECIAL, 35, 85, 15, -1, 0, 1) new AttackMove(Moves.FIRE_SPIN, Type.FIRE, MoveCategory.SPECIAL, 35, 85, 15, -1, 0, 1)
@ -8517,7 +8544,8 @@ export function initMoves() {
.attr(StatusEffectAttr, StatusEffect.PARALYSIS), .attr(StatusEffectAttr, StatusEffect.PARALYSIS),
new StatusMove(Moves.THUNDER_WAVE, Type.ELECTRIC, 90, 20, -1, 0, 1) new StatusMove(Moves.THUNDER_WAVE, Type.ELECTRIC, 90, 20, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.PARALYSIS) .attr(StatusEffectAttr, StatusEffect.PARALYSIS)
.attr(RespectAttackTypeImmunityAttr), .attr(RespectAttackTypeImmunityAttr)
.reflectable(),
new AttackMove(Moves.THUNDER, Type.ELECTRIC, MoveCategory.SPECIAL, 110, 70, 10, 30, 0, 1) new AttackMove(Moves.THUNDER, Type.ELECTRIC, MoveCategory.SPECIAL, 110, 70, 10, 30, 0, 1)
.attr(StatusEffectAttr, StatusEffect.PARALYSIS) .attr(StatusEffectAttr, StatusEffect.PARALYSIS)
.attr(ThunderAccuracyAttr) .attr(ThunderAccuracyAttr)
@ -8539,13 +8567,15 @@ export function initMoves() {
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.UNDERGROUND), .chargeAttr(SemiInvulnerableAttr, BattlerTagType.UNDERGROUND),
new StatusMove(Moves.TOXIC, Type.POISON, 90, 10, -1, 0, 1) new StatusMove(Moves.TOXIC, Type.POISON, 90, 10, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.TOXIC) .attr(StatusEffectAttr, StatusEffect.TOXIC)
.attr(ToxicAccuracyAttr), .attr(ToxicAccuracyAttr)
.reflectable(),
new AttackMove(Moves.CONFUSION, Type.PSYCHIC, MoveCategory.SPECIAL, 50, 100, 25, 10, 0, 1) new AttackMove(Moves.CONFUSION, Type.PSYCHIC, MoveCategory.SPECIAL, 50, 100, 25, 10, 0, 1)
.attr(ConfuseAttr), .attr(ConfuseAttr),
new AttackMove(Moves.PSYCHIC, Type.PSYCHIC, MoveCategory.SPECIAL, 90, 100, 10, 10, 0, 1) new AttackMove(Moves.PSYCHIC, Type.PSYCHIC, MoveCategory.SPECIAL, 90, 100, 10, 10, 0, 1)
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -1), .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1),
new StatusMove(Moves.HYPNOSIS, Type.PSYCHIC, 60, 20, -1, 0, 1) new StatusMove(Moves.HYPNOSIS, Type.PSYCHIC, 60, 20, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.SLEEP), .attr(StatusEffectAttr, StatusEffect.SLEEP)
.reflectable(),
new SelfStatusMove(Moves.MEDITATE, Type.PSYCHIC, -1, 40, -1, 0, 1) new SelfStatusMove(Moves.MEDITATE, Type.PSYCHIC, -1, 40, -1, 0, 1)
.attr(StatStageChangeAttr, [ Stat.ATK ], 1, true), .attr(StatStageChangeAttr, [ Stat.ATK ], 1, true),
new SelfStatusMove(Moves.AGILITY, Type.PSYCHIC, -1, 30, -1, 0, 1) new SelfStatusMove(Moves.AGILITY, Type.PSYCHIC, -1, 30, -1, 0, 1)
@ -8563,7 +8593,8 @@ export function initMoves() {
.ignoresSubstitute(), .ignoresSubstitute(),
new StatusMove(Moves.SCREECH, Type.NORMAL, 85, 40, -1, 0, 1) new StatusMove(Moves.SCREECH, Type.NORMAL, 85, 40, -1, 0, 1)
.attr(StatStageChangeAttr, [ Stat.DEF ], -2) .attr(StatStageChangeAttr, [ Stat.DEF ], -2)
.soundBased(), .soundBased()
.reflectable(),
new SelfStatusMove(Moves.DOUBLE_TEAM, Type.NORMAL, -1, 15, -1, 0, 1) new SelfStatusMove(Moves.DOUBLE_TEAM, Type.NORMAL, -1, 15, -1, 0, 1)
.attr(StatStageChangeAttr, [ Stat.EVA ], 1, true), .attr(StatStageChangeAttr, [ Stat.EVA ], 1, true),
new SelfStatusMove(Moves.RECOVER, Type.NORMAL, -1, 5, -1, 0, 1) new SelfStatusMove(Moves.RECOVER, Type.NORMAL, -1, 5, -1, 0, 1)
@ -8575,9 +8606,11 @@ export function initMoves() {
.attr(AddBattlerTagAttr, BattlerTagType.MINIMIZED, true, false) .attr(AddBattlerTagAttr, BattlerTagType.MINIMIZED, true, false)
.attr(StatStageChangeAttr, [ Stat.EVA ], 2, true), .attr(StatStageChangeAttr, [ Stat.EVA ], 2, true),
new StatusMove(Moves.SMOKESCREEN, Type.NORMAL, 100, 20, -1, 0, 1) new StatusMove(Moves.SMOKESCREEN, Type.NORMAL, 100, 20, -1, 0, 1)
.attr(StatStageChangeAttr, [ Stat.ACC ], -1), .attr(StatStageChangeAttr, [ Stat.ACC ], -1)
.reflectable(),
new StatusMove(Moves.CONFUSE_RAY, Type.GHOST, 100, 10, -1, 0, 1) new StatusMove(Moves.CONFUSE_RAY, Type.GHOST, 100, 10, -1, 0, 1)
.attr(ConfuseAttr), .attr(ConfuseAttr)
.reflectable(),
new SelfStatusMove(Moves.WITHDRAW, Type.WATER, -1, 40, -1, 0, 1) new SelfStatusMove(Moves.WITHDRAW, Type.WATER, -1, 40, -1, 0, 1)
.attr(StatStageChangeAttr, [ Stat.DEF ], 1, true), .attr(StatStageChangeAttr, [ Stat.DEF ], 1, true),
new SelfStatusMove(Moves.DEFENSE_CURL, Type.NORMAL, -1, 40, -1, 0, 1) new SelfStatusMove(Moves.DEFENSE_CURL, Type.NORMAL, -1, 40, -1, 0, 1)
@ -8638,7 +8671,8 @@ export function initMoves() {
new SelfStatusMove(Moves.AMNESIA, Type.PSYCHIC, -1, 20, -1, 0, 1) new SelfStatusMove(Moves.AMNESIA, Type.PSYCHIC, -1, 20, -1, 0, 1)
.attr(StatStageChangeAttr, [ Stat.SPDEF ], 2, true), .attr(StatStageChangeAttr, [ Stat.SPDEF ], 2, true),
new StatusMove(Moves.KINESIS, Type.PSYCHIC, 80, 15, -1, 0, 1) new StatusMove(Moves.KINESIS, Type.PSYCHIC, 80, 15, -1, 0, 1)
.attr(StatStageChangeAttr, [ Stat.ACC ], -1), .attr(StatStageChangeAttr, [ Stat.ACC ], -1)
.reflectable(),
new SelfStatusMove(Moves.SOFT_BOILED, Type.NORMAL, -1, 5, -1, 0, 1) new SelfStatusMove(Moves.SOFT_BOILED, Type.NORMAL, -1, 5, -1, 0, 1)
.attr(HealAttr, 0.5) .attr(HealAttr, 0.5)
.triageMove(), .triageMove(),
@ -8648,14 +8682,16 @@ export function initMoves() {
.condition(failOnGravityCondition) .condition(failOnGravityCondition)
.recklessMove(), .recklessMove(),
new StatusMove(Moves.GLARE, Type.NORMAL, 100, 30, -1, 0, 1) new StatusMove(Moves.GLARE, Type.NORMAL, 100, 30, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.PARALYSIS), .attr(StatusEffectAttr, StatusEffect.PARALYSIS)
.reflectable(),
new AttackMove(Moves.DREAM_EATER, Type.PSYCHIC, MoveCategory.SPECIAL, 100, 100, 15, -1, 0, 1) new AttackMove(Moves.DREAM_EATER, Type.PSYCHIC, MoveCategory.SPECIAL, 100, 100, 15, -1, 0, 1)
.attr(HitHealAttr) .attr(HitHealAttr)
.condition(targetSleptOrComatoseCondition) .condition(targetSleptOrComatoseCondition)
.triageMove(), .triageMove(),
new StatusMove(Moves.POISON_GAS, Type.POISON, 90, 40, -1, 0, 1) new StatusMove(Moves.POISON_GAS, Type.POISON, 90, 40, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.POISON) .attr(StatusEffectAttr, StatusEffect.POISON)
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES)
.reflectable(),
new AttackMove(Moves.BARRAGE, Type.NORMAL, MoveCategory.PHYSICAL, 15, 85, 20, -1, 0, 1) new AttackMove(Moves.BARRAGE, Type.NORMAL, MoveCategory.PHYSICAL, 15, 85, 20, -1, 0, 1)
.attr(MultiHitAttr) .attr(MultiHitAttr)
.makesContact(false) .makesContact(false)
@ -8664,7 +8700,8 @@ export function initMoves() {
.attr(HitHealAttr) .attr(HitHealAttr)
.triageMove(), .triageMove(),
new StatusMove(Moves.LOVELY_KISS, Type.NORMAL, 75, 10, -1, 0, 1) new StatusMove(Moves.LOVELY_KISS, Type.NORMAL, 75, 10, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.SLEEP), .attr(StatusEffectAttr, StatusEffect.SLEEP)
.reflectable(),
new ChargingAttackMove(Moves.SKY_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 140, 90, 5, 30, 0, 1) new ChargingAttackMove(Moves.SKY_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 140, 90, 5, 30, 0, 1)
.chargeText(i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" })) .chargeText(i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" }))
.attr(HighCritAttr) .attr(HighCritAttr)
@ -8683,9 +8720,11 @@ export function initMoves() {
.punchingMove(), .punchingMove(),
new StatusMove(Moves.SPORE, Type.GRASS, 100, 15, -1, 0, 1) new StatusMove(Moves.SPORE, Type.GRASS, 100, 15, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.SLEEP) .attr(StatusEffectAttr, StatusEffect.SLEEP)
.powderMove(), .powderMove()
.reflectable(),
new StatusMove(Moves.FLASH, Type.NORMAL, 100, 20, -1, 0, 1) new StatusMove(Moves.FLASH, Type.NORMAL, 100, 20, -1, 0, 1)
.attr(StatStageChangeAttr, [ Stat.ACC ], -1), .attr(StatStageChangeAttr, [ Stat.ACC ], -1)
.reflectable(),
new AttackMove(Moves.PSYWAVE, Type.PSYCHIC, MoveCategory.SPECIAL, -1, 100, 15, -1, 0, 1) new AttackMove(Moves.PSYWAVE, Type.PSYCHIC, MoveCategory.SPECIAL, -1, 100, 15, -1, 0, 1)
.attr(RandomLevelDamageAttr), .attr(RandomLevelDamageAttr),
new SelfStatusMove(Moves.SPLASH, Type.NORMAL, -1, 40, -1, 0, 1) new SelfStatusMove(Moves.SPLASH, Type.NORMAL, -1, 40, -1, 0, 1)
@ -8744,7 +8783,8 @@ export function initMoves() {
.attr(StealHeldItemChanceAttr, 0.3), .attr(StealHeldItemChanceAttr, 0.3),
new StatusMove(Moves.SPIDER_WEB, Type.BUG, -1, 10, -1, 0, 2) new StatusMove(Moves.SPIDER_WEB, Type.BUG, -1, 10, -1, 0, 2)
.condition(failIfGhostTypeCondition) .condition(failIfGhostTypeCondition)
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1), .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1)
.reflectable(),
new StatusMove(Moves.MIND_READER, Type.NORMAL, -1, 5, -1, 0, 2) new StatusMove(Moves.MIND_READER, Type.NORMAL, -1, 5, -1, 0, 2)
.attr(IgnoreAccuracyAttr), .attr(IgnoreAccuracyAttr),
new StatusMove(Moves.NIGHTMARE, Type.GHOST, 100, 15, -1, 0, 2) new StatusMove(Moves.NIGHTMARE, Type.GHOST, 100, 15, -1, 0, 2)
@ -8775,12 +8815,14 @@ export function initMoves() {
new StatusMove(Moves.COTTON_SPORE, Type.GRASS, 100, 40, -1, 0, 2) new StatusMove(Moves.COTTON_SPORE, Type.GRASS, 100, 40, -1, 0, 2)
.attr(StatStageChangeAttr, [ Stat.SPD ], -2) .attr(StatStageChangeAttr, [ Stat.SPD ], -2)
.powderMove() .powderMove()
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES)
.reflectable(),
new AttackMove(Moves.REVERSAL, Type.FIGHTING, MoveCategory.PHYSICAL, -1, 100, 15, -1, 0, 2) new AttackMove(Moves.REVERSAL, Type.FIGHTING, MoveCategory.PHYSICAL, -1, 100, 15, -1, 0, 2)
.attr(LowHpPowerAttr), .attr(LowHpPowerAttr),
new StatusMove(Moves.SPITE, Type.GHOST, 100, 10, -1, 0, 2) new StatusMove(Moves.SPITE, Type.GHOST, 100, 10, -1, 0, 2)
.ignoresSubstitute() .ignoresSubstitute()
.attr(ReducePpMoveAttr, 4), .attr(ReducePpMoveAttr, 4)
.reflectable(),
new AttackMove(Moves.POWDER_SNOW, Type.ICE, MoveCategory.SPECIAL, 40, 100, 25, 10, 0, 2) new AttackMove(Moves.POWDER_SNOW, Type.ICE, MoveCategory.SPECIAL, 40, 100, 25, 10, 0, 2)
.attr(StatusEffectAttr, StatusEffect.FREEZE) .attr(StatusEffectAttr, StatusEffect.FREEZE)
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
@ -8790,10 +8832,12 @@ export function initMoves() {
new AttackMove(Moves.MACH_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 40, 100, 30, -1, 1, 2) new AttackMove(Moves.MACH_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 40, 100, 30, -1, 1, 2)
.punchingMove(), .punchingMove(),
new StatusMove(Moves.SCARY_FACE, Type.NORMAL, 100, 10, -1, 0, 2) new StatusMove(Moves.SCARY_FACE, Type.NORMAL, 100, 10, -1, 0, 2)
.attr(StatStageChangeAttr, [ Stat.SPD ], -2), .attr(StatStageChangeAttr, [ Stat.SPD ], -2)
.reflectable(),
new AttackMove(Moves.FEINT_ATTACK, Type.DARK, MoveCategory.PHYSICAL, 60, -1, 20, -1, 0, 2), new AttackMove(Moves.FEINT_ATTACK, Type.DARK, MoveCategory.PHYSICAL, 60, -1, 20, -1, 0, 2),
new StatusMove(Moves.SWEET_KISS, Type.FAIRY, 75, 10, -1, 0, 2) new StatusMove(Moves.SWEET_KISS, Type.FAIRY, 75, 10, -1, 0, 2)
.attr(ConfuseAttr), .attr(ConfuseAttr)
.reflectable(),
new SelfStatusMove(Moves.BELLY_DRUM, Type.NORMAL, -1, 10, -1, 0, 2) new SelfStatusMove(Moves.BELLY_DRUM, Type.NORMAL, -1, 10, -1, 0, 2)
.attr(CutHpStatStageBoostAttr, [ Stat.ATK ], 12, 2, (user) => { .attr(CutHpStatStageBoostAttr, [ Stat.ATK ], 12, 2, (user) => {
globalScene.queueMessage(i18next.t("moveTriggers:cutOwnHpAndMaximizedStat", { pokemonName: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.ATK)) })); globalScene.queueMessage(i18next.t("moveTriggers:cutOwnHpAndMaximizedStat", { pokemonName: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.ATK)) }));
@ -8808,13 +8852,15 @@ export function initMoves() {
.ballBombMove(), .ballBombMove(),
new StatusMove(Moves.SPIKES, Type.GROUND, -1, 20, -1, 0, 2) new StatusMove(Moves.SPIKES, Type.GROUND, -1, 20, -1, 0, 2)
.attr(AddArenaTrapTagAttr, ArenaTagType.SPIKES) .attr(AddArenaTrapTagAttr, ArenaTagType.SPIKES)
.target(MoveTarget.ENEMY_SIDE), .target(MoveTarget.ENEMY_SIDE)
.reflectable(),
new AttackMove(Moves.ZAP_CANNON, Type.ELECTRIC, MoveCategory.SPECIAL, 120, 50, 5, 100, 0, 2) new AttackMove(Moves.ZAP_CANNON, Type.ELECTRIC, MoveCategory.SPECIAL, 120, 50, 5, 100, 0, 2)
.attr(StatusEffectAttr, StatusEffect.PARALYSIS) .attr(StatusEffectAttr, StatusEffect.PARALYSIS)
.ballBombMove(), .ballBombMove(),
new StatusMove(Moves.FORESIGHT, Type.NORMAL, -1, 40, -1, 0, 2) new StatusMove(Moves.FORESIGHT, Type.NORMAL, -1, 40, -1, 0, 2)
.attr(ExposedMoveAttr, BattlerTagType.IGNORE_GHOST) .attr(ExposedMoveAttr, BattlerTagType.IGNORE_GHOST)
.ignoresSubstitute(), .ignoresSubstitute()
.reflectable(),
new SelfStatusMove(Moves.DESTINY_BOND, Type.GHOST, -1, 5, -1, 0, 2) new SelfStatusMove(Moves.DESTINY_BOND, Type.GHOST, -1, 5, -1, 0, 2)
.ignoresProtect() .ignoresProtect()
.attr(DestinyBondAttr) .attr(DestinyBondAttr)
@ -8860,7 +8906,8 @@ export function initMoves() {
.attr(ProtectAttr, BattlerTagType.ENDURING) .attr(ProtectAttr, BattlerTagType.ENDURING)
.condition(failIfLastCondition), .condition(failIfLastCondition),
new StatusMove(Moves.CHARM, Type.FAIRY, 100, 20, -1, 0, 2) new StatusMove(Moves.CHARM, Type.FAIRY, 100, 20, -1, 0, 2)
.attr(StatStageChangeAttr, [ Stat.ATK ], -2), .attr(StatStageChangeAttr, [ Stat.ATK ], -2)
.reflectable(),
new AttackMove(Moves.ROLLOUT, Type.ROCK, MoveCategory.PHYSICAL, 30, 90, 20, -1, 0, 2) new AttackMove(Moves.ROLLOUT, Type.ROCK, MoveCategory.PHYSICAL, 30, 90, 20, -1, 0, 2)
.partial() // Does not lock the user, also does not increase damage properly .partial() // Does not lock the user, also does not increase damage properly
.attr(ConsecutiveUseDoublePowerAttr, 5, true, true, Moves.DEFENSE_CURL), .attr(ConsecutiveUseDoublePowerAttr, 5, true, true, Moves.DEFENSE_CURL),
@ -8868,7 +8915,8 @@ export function initMoves() {
.attr(SurviveDamageAttr), .attr(SurviveDamageAttr),
new StatusMove(Moves.SWAGGER, Type.NORMAL, 85, 15, -1, 0, 2) new StatusMove(Moves.SWAGGER, Type.NORMAL, 85, 15, -1, 0, 2)
.attr(StatStageChangeAttr, [ Stat.ATK ], 2) .attr(StatStageChangeAttr, [ Stat.ATK ], 2)
.attr(ConfuseAttr), .attr(ConfuseAttr)
.reflectable(),
new SelfStatusMove(Moves.MILK_DRINK, Type.NORMAL, -1, 5, -1, 0, 2) new SelfStatusMove(Moves.MILK_DRINK, Type.NORMAL, -1, 5, -1, 0, 2)
.attr(HealAttr, 0.5) .attr(HealAttr, 0.5)
.triageMove(), .triageMove(),
@ -8881,11 +8929,13 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.DEF ], 1, true), .attr(StatStageChangeAttr, [ Stat.DEF ], 1, true),
new StatusMove(Moves.MEAN_LOOK, Type.NORMAL, -1, 5, -1, 0, 2) new StatusMove(Moves.MEAN_LOOK, Type.NORMAL, -1, 5, -1, 0, 2)
.condition(failIfGhostTypeCondition) .condition(failIfGhostTypeCondition)
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1), .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1)
.reflectable(),
new StatusMove(Moves.ATTRACT, Type.NORMAL, 100, 15, -1, 0, 2) new StatusMove(Moves.ATTRACT, Type.NORMAL, 100, 15, -1, 0, 2)
.attr(AddBattlerTagAttr, BattlerTagType.INFATUATED) .attr(AddBattlerTagAttr, BattlerTagType.INFATUATED)
.ignoresSubstitute() .ignoresSubstitute()
.condition((user, target, move) => user.isOppositeGender(target)), .condition((user, target, move) => user.isOppositeGender(target))
.reflectable(),
new SelfStatusMove(Moves.SLEEP_TALK, Type.NORMAL, -1, 10, -1, 0, 2) new SelfStatusMove(Moves.SLEEP_TALK, Type.NORMAL, -1, 10, -1, 0, 2)
.attr(BypassSleepAttr) .attr(BypassSleepAttr)
.attr(RandomMovesetMoveAttr, invalidSleepTalkMoves, false) .attr(RandomMovesetMoveAttr, invalidSleepTalkMoves, false)
@ -8932,7 +8982,8 @@ export function initMoves() {
new StatusMove(Moves.ENCORE, Type.NORMAL, 100, 5, -1, 0, 2) new StatusMove(Moves.ENCORE, Type.NORMAL, 100, 5, -1, 0, 2)
.attr(AddBattlerTagAttr, BattlerTagType.ENCORE, false, true) .attr(AddBattlerTagAttr, BattlerTagType.ENCORE, false, true)
.ignoresSubstitute() .ignoresSubstitute()
.condition((user, target, move) => new EncoreTag(user.id).canAdd(target)), .condition((user, target, move) => new EncoreTag(user.id).canAdd(target))
.reflectable(),
new AttackMove(Moves.PURSUIT, Type.DARK, MoveCategory.PHYSICAL, 40, 100, 20, -1, 0, 2) new AttackMove(Moves.PURSUIT, Type.DARK, MoveCategory.PHYSICAL, 40, 100, 20, -1, 0, 2)
.partial(), // No effect implemented .partial(), // No effect implemented
new AttackMove(Moves.RAPID_SPIN, Type.NORMAL, MoveCategory.PHYSICAL, 50, 100, 40, 100, 0, 2) new AttackMove(Moves.RAPID_SPIN, Type.NORMAL, MoveCategory.PHYSICAL, 50, 100, 40, 100, 0, 2)
@ -8953,7 +9004,8 @@ export function initMoves() {
.attr(RemoveArenaTrapAttr), .attr(RemoveArenaTrapAttr),
new StatusMove(Moves.SWEET_SCENT, Type.NORMAL, 100, 20, -1, 0, 2) new StatusMove(Moves.SWEET_SCENT, Type.NORMAL, 100, 20, -1, 0, 2)
.attr(StatStageChangeAttr, [ Stat.EVA ], -2) .attr(StatStageChangeAttr, [ Stat.EVA ], -2)
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES)
.reflectable(),
new AttackMove(Moves.IRON_TAIL, Type.STEEL, MoveCategory.PHYSICAL, 100, 75, 15, 30, 0, 2) new AttackMove(Moves.IRON_TAIL, Type.STEEL, MoveCategory.PHYSICAL, 100, 75, 15, 30, 0, 2)
.attr(StatStageChangeAttr, [ Stat.DEF ], -1), .attr(StatStageChangeAttr, [ Stat.DEF ], -1),
new AttackMove(Moves.METAL_CLAW, Type.STEEL, MoveCategory.PHYSICAL, 50, 95, 35, 10, 0, 2) new AttackMove(Moves.METAL_CLAW, Type.STEEL, MoveCategory.PHYSICAL, 50, 95, 35, 10, 0, 2)
@ -9041,12 +9093,15 @@ export function initMoves() {
new StatusMove(Moves.TORMENT, Type.DARK, 100, 15, -1, 0, 3) new StatusMove(Moves.TORMENT, Type.DARK, 100, 15, -1, 0, 3)
.ignoresSubstitute() .ignoresSubstitute()
.edgeCase() // Incomplete implementation because of Uproar's partial implementation .edgeCase() // Incomplete implementation because of Uproar's partial implementation
.attr(AddBattlerTagAttr, BattlerTagType.TORMENT, false, true, 1), .attr(AddBattlerTagAttr, BattlerTagType.TORMENT, false, true, 1)
.reflectable(),
new StatusMove(Moves.FLATTER, Type.DARK, 100, 15, -1, 0, 3) new StatusMove(Moves.FLATTER, Type.DARK, 100, 15, -1, 0, 3)
.attr(StatStageChangeAttr, [ Stat.SPATK ], 1) .attr(StatStageChangeAttr, [ Stat.SPATK ], 1)
.attr(ConfuseAttr), .attr(ConfuseAttr)
.reflectable(),
new StatusMove(Moves.WILL_O_WISP, Type.FIRE, 85, 15, -1, 0, 3) new StatusMove(Moves.WILL_O_WISP, Type.FIRE, 85, 15, -1, 0, 3)
.attr(StatusEffectAttr, StatusEffect.BURN), .attr(StatusEffectAttr, StatusEffect.BURN)
.reflectable(),
new StatusMove(Moves.MEMENTO, Type.DARK, 100, 10, -1, 0, 3) new StatusMove(Moves.MEMENTO, Type.DARK, 100, 10, -1, 0, 3)
.attr(SacrificialAttrOnHit) .attr(SacrificialAttrOnHit)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -2), .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -2),
@ -9070,7 +9125,8 @@ export function initMoves() {
.attr(AddBattlerTagAttr, BattlerTagType.CHARGED, true, false), .attr(AddBattlerTagAttr, BattlerTagType.CHARGED, true, false),
new StatusMove(Moves.TAUNT, Type.DARK, 100, 20, -1, 0, 3) new StatusMove(Moves.TAUNT, Type.DARK, 100, 20, -1, 0, 3)
.ignoresSubstitute() .ignoresSubstitute()
.attr(AddBattlerTagAttr, BattlerTagType.TAUNT, false, true, 4), .attr(AddBattlerTagAttr, BattlerTagType.TAUNT, false, true, 4)
.reflectable(),
new StatusMove(Moves.HELPING_HAND, Type.NORMAL, -1, 20, -1, 5, 3) new StatusMove(Moves.HELPING_HAND, Type.NORMAL, -1, 20, -1, 5, 3)
.attr(AddBattlerTagAttr, BattlerTagType.HELPING_HAND) .attr(AddBattlerTagAttr, BattlerTagType.HELPING_HAND)
.ignoresSubstitute() .ignoresSubstitute()
@ -9093,7 +9149,12 @@ export function initMoves() {
new AttackMove(Moves.SUPERPOWER, Type.FIGHTING, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 3) new AttackMove(Moves.SUPERPOWER, Type.FIGHTING, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 3)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], -1, true), .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], -1, true),
new SelfStatusMove(Moves.MAGIC_COAT, Type.PSYCHIC, -1, 15, -1, 4, 3) new SelfStatusMove(Moves.MAGIC_COAT, Type.PSYCHIC, -1, 15, -1, 4, 3)
.unimplemented(), .attr(AddBattlerTagAttr, BattlerTagType.MAGIC_COAT, true, true, 0)
.condition(failIfLastCondition)
// 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) new SelfStatusMove(Moves.RECYCLE, Type.NORMAL, -1, 10, -1, 0, 3)
.unimplemented(), .unimplemented(),
new AttackMove(Moves.REVENGE, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 100, 10, -1, -4, 3) new AttackMove(Moves.REVENGE, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 100, 10, -1, -4, 3)
@ -9102,7 +9163,8 @@ export function initMoves() {
.attr(RemoveScreensAttr), .attr(RemoveScreensAttr),
new StatusMove(Moves.YAWN, Type.NORMAL, -1, 10, -1, 0, 3) new StatusMove(Moves.YAWN, Type.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(),
new AttackMove(Moves.KNOCK_OFF, Type.DARK, MoveCategory.PHYSICAL, 65, 100, 20, -1, 0, 3) new AttackMove(Moves.KNOCK_OFF, Type.DARK, MoveCategory.PHYSICAL, 65, 100, 20, -1, 0, 3)
.attr(MovePowerMultiplierAttr, (user, target, move) => target.getHeldItems().filter(i => i.isTransferable).length > 0 ? 1.5 : 1) .attr(MovePowerMultiplierAttr, (user, target, move) => target.getHeldItems().filter(i => i.isTransferable).length > 0 ? 1.5 : 1)
.attr(RemoveHeldItemAttr, false), .attr(RemoveHeldItemAttr, false),
@ -9146,7 +9208,8 @@ export function initMoves() {
.ballBombMove(), .ballBombMove(),
new StatusMove(Moves.FEATHER_DANCE, Type.FLYING, 100, 15, -1, 0, 3) new StatusMove(Moves.FEATHER_DANCE, Type.FLYING, 100, 15, -1, 0, 3)
.attr(StatStageChangeAttr, [ Stat.ATK ], -2) .attr(StatStageChangeAttr, [ Stat.ATK ], -2)
.danceMove(), .danceMove()
.reflectable(),
new StatusMove(Moves.TEETER_DANCE, Type.NORMAL, 100, 20, -1, 0, 3) new StatusMove(Moves.TEETER_DANCE, Type.NORMAL, 100, 20, -1, 0, 3)
.attr(ConfuseAttr) .attr(ConfuseAttr)
.danceMove() .danceMove()
@ -9192,7 +9255,8 @@ export function initMoves() {
.attr(PartyStatusCureAttr, i18next.t("moveTriggers:soothingAromaWaftedThroughArea"), Abilities.SAP_SIPPER) .attr(PartyStatusCureAttr, i18next.t("moveTriggers:soothingAromaWaftedThroughArea"), Abilities.SAP_SIPPER)
.target(MoveTarget.PARTY), .target(MoveTarget.PARTY),
new StatusMove(Moves.FAKE_TEARS, Type.DARK, 100, 20, -1, 0, 3) new StatusMove(Moves.FAKE_TEARS, Type.DARK, 100, 20, -1, 0, 3)
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -2), .attr(StatStageChangeAttr, [ Stat.SPDEF ], -2)
.reflectable(),
new AttackMove(Moves.AIR_CUTTER, Type.FLYING, MoveCategory.SPECIAL, 60, 95, 25, -1, 0, 3) new AttackMove(Moves.AIR_CUTTER, Type.FLYING, MoveCategory.SPECIAL, 60, 95, 25, -1, 0, 3)
.attr(HighCritAttr) .attr(HighCritAttr)
.slicingMove() .slicingMove()
@ -9203,7 +9267,8 @@ export function initMoves() {
.attr(HealStatusEffectAttr, true, StatusEffect.FREEZE), .attr(HealStatusEffectAttr, true, StatusEffect.FREEZE),
new StatusMove(Moves.ODOR_SLEUTH, Type.NORMAL, -1, 40, -1, 0, 3) new StatusMove(Moves.ODOR_SLEUTH, Type.NORMAL, -1, 40, -1, 0, 3)
.attr(ExposedMoveAttr, BattlerTagType.IGNORE_GHOST) .attr(ExposedMoveAttr, BattlerTagType.IGNORE_GHOST)
.ignoresSubstitute(), .ignoresSubstitute()
.reflectable(),
new AttackMove(Moves.ROCK_TOMB, Type.ROCK, MoveCategory.PHYSICAL, 60, 95, 15, 100, 0, 3) new AttackMove(Moves.ROCK_TOMB, Type.ROCK, MoveCategory.PHYSICAL, 60, 95, 15, 100, 0, 3)
.attr(StatStageChangeAttr, [ Stat.SPD ], -1) .attr(StatStageChangeAttr, [ Stat.SPD ], -1)
.makesContact(false), .makesContact(false),
@ -9212,12 +9277,15 @@ export function initMoves() {
.windMove(), .windMove(),
new StatusMove(Moves.METAL_SOUND, Type.STEEL, 85, 40, -1, 0, 3) new StatusMove(Moves.METAL_SOUND, Type.STEEL, 85, 40, -1, 0, 3)
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -2) .attr(StatStageChangeAttr, [ Stat.SPDEF ], -2)
.soundBased(), .soundBased()
.reflectable(),
new StatusMove(Moves.GRASS_WHISTLE, Type.GRASS, 55, 15, -1, 0, 3) new StatusMove(Moves.GRASS_WHISTLE, Type.GRASS, 55, 15, -1, 0, 3)
.attr(StatusEffectAttr, StatusEffect.SLEEP) .attr(StatusEffectAttr, StatusEffect.SLEEP)
.soundBased(), .soundBased()
.reflectable(),
new StatusMove(Moves.TICKLE, Type.NORMAL, 100, 20, -1, 0, 3) new StatusMove(Moves.TICKLE, Type.NORMAL, 100, 20, -1, 0, 3)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], -1), .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], -1)
.reflectable(),
new SelfStatusMove(Moves.COSMIC_POWER, Type.PSYCHIC, -1, 20, -1, 0, 3) new SelfStatusMove(Moves.COSMIC_POWER, Type.PSYCHIC, -1, 20, -1, 0, 3)
.attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, true), .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, true),
new AttackMove(Moves.WATER_SPOUT, Type.WATER, MoveCategory.SPECIAL, 150, 100, 5, -1, 0, 3) new AttackMove(Moves.WATER_SPOUT, Type.WATER, MoveCategory.SPECIAL, 150, 100, 5, -1, 0, 3)
@ -9255,7 +9323,8 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.DEF ], 2, true), .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true),
new StatusMove(Moves.BLOCK, Type.NORMAL, -1, 5, -1, 0, 3) new StatusMove(Moves.BLOCK, Type.NORMAL, -1, 5, -1, 0, 3)
.condition(failIfGhostTypeCondition) .condition(failIfGhostTypeCondition)
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1), .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1)
.reflectable(),
new StatusMove(Moves.HOWL, Type.NORMAL, -1, 40, -1, 0, 3) new StatusMove(Moves.HOWL, Type.NORMAL, -1, 40, -1, 0, 3)
.attr(StatStageChangeAttr, [ Stat.ATK ], 1) .attr(StatStageChangeAttr, [ Stat.ATK ], 1)
.soundBased() .soundBased()
@ -9318,7 +9387,8 @@ export function initMoves() {
.target(MoveTarget.BOTH_SIDES), .target(MoveTarget.BOTH_SIDES),
new StatusMove(Moves.MIRACLE_EYE, Type.PSYCHIC, -1, 40, -1, 0, 4) new StatusMove(Moves.MIRACLE_EYE, Type.PSYCHIC, -1, 40, -1, 0, 4)
.attr(ExposedMoveAttr, BattlerTagType.IGNORE_DARK) .attr(ExposedMoveAttr, BattlerTagType.IGNORE_DARK)
.ignoresSubstitute(), .ignoresSubstitute()
.reflectable(),
new AttackMove(Moves.WAKE_UP_SLAP, Type.FIGHTING, MoveCategory.PHYSICAL, 70, 100, 10, -1, 0, 4) new AttackMove(Moves.WAKE_UP_SLAP, Type.FIGHTING, MoveCategory.PHYSICAL, 70, 100, 10, -1, 0, 4)
.attr(MovePowerMultiplierAttr, (user, target, move) => targetSleptOrComatoseCondition(user, target, move) ? 2 : 1) .attr(MovePowerMultiplierAttr, (user, target, move) => targetSleptOrComatoseCondition(user, target, move) ? 2 : 1)
.attr(HealStatusEffectAttr, false, StatusEffect.SLEEP), .attr(HealStatusEffectAttr, false, StatusEffect.SLEEP),
@ -9364,6 +9434,7 @@ export function initMoves() {
new AttackMove(Moves.ASSURANCE, Type.DARK, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 4) new AttackMove(Moves.ASSURANCE, Type.DARK, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 4)
.attr(MovePowerMultiplierAttr, (user, target, move) => target.turnData.damageTaken > 0 ? 2 : 1), .attr(MovePowerMultiplierAttr, (user, target, move) => target.turnData.damageTaken > 0 ? 2 : 1),
new StatusMove(Moves.EMBARGO, Type.DARK, 100, 15, -1, 0, 4) new StatusMove(Moves.EMBARGO, Type.DARK, 100, 15, -1, 0, 4)
.reflectable()
.unimplemented(), .unimplemented(),
new AttackMove(Moves.FLING, Type.DARK, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 4) new AttackMove(Moves.FLING, Type.DARK, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 4)
.makesContact(false) .makesContact(false)
@ -9383,14 +9454,16 @@ export function initMoves() {
.attr(LessPPMorePowerAttr), .attr(LessPPMorePowerAttr),
new StatusMove(Moves.HEAL_BLOCK, Type.PSYCHIC, 100, 15, -1, 0, 4) new StatusMove(Moves.HEAL_BLOCK, Type.PSYCHIC, 100, 15, -1, 0, 4)
.attr(AddBattlerTagAttr, BattlerTagType.HEAL_BLOCK, false, true, 5) .attr(AddBattlerTagAttr, BattlerTagType.HEAL_BLOCK, false, true, 5)
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES)
.reflectable(),
new AttackMove(Moves.WRING_OUT, Type.NORMAL, MoveCategory.SPECIAL, -1, 100, 5, -1, 0, 4) new AttackMove(Moves.WRING_OUT, Type.NORMAL, MoveCategory.SPECIAL, -1, 100, 5, -1, 0, 4)
.attr(OpponentHighHpPowerAttr, 120) .attr(OpponentHighHpPowerAttr, 120)
.makesContact(), .makesContact(),
new SelfStatusMove(Moves.POWER_TRICK, Type.PSYCHIC, -1, 10, -1, 0, 4) new SelfStatusMove(Moves.POWER_TRICK, Type.PSYCHIC, -1, 10, -1, 0, 4)
.attr(AddBattlerTagAttr, BattlerTagType.POWER_TRICK, true), .attr(AddBattlerTagAttr, BattlerTagType.POWER_TRICK, true),
new StatusMove(Moves.GASTRO_ACID, Type.POISON, 100, 10, -1, 0, 4) new StatusMove(Moves.GASTRO_ACID, Type.POISON, 100, 10, -1, 0, 4)
.attr(SuppressAbilitiesAttr), .attr(SuppressAbilitiesAttr)
.reflectable(),
new StatusMove(Moves.LUCKY_CHANT, Type.NORMAL, -1, 30, -1, 0, 4) new StatusMove(Moves.LUCKY_CHANT, Type.NORMAL, -1, 30, -1, 0, 4)
.attr(AddArenaTagAttr, ArenaTagType.NO_CRIT, 5, true, true) .attr(AddArenaTagAttr, ArenaTagType.NO_CRIT, 5, true, true)
.target(MoveTarget.USER_SIDE), .target(MoveTarget.USER_SIDE),
@ -9412,12 +9485,14 @@ export function initMoves() {
new AttackMove(Moves.LAST_RESORT, Type.NORMAL, MoveCategory.PHYSICAL, 140, 100, 5, -1, 0, 4) new AttackMove(Moves.LAST_RESORT, Type.NORMAL, MoveCategory.PHYSICAL, 140, 100, 5, -1, 0, 4)
.attr(LastResortAttr), .attr(LastResortAttr),
new StatusMove(Moves.WORRY_SEED, Type.GRASS, 100, 10, -1, 0, 4) new StatusMove(Moves.WORRY_SEED, Type.GRASS, 100, 10, -1, 0, 4)
.attr(AbilityChangeAttr, Abilities.INSOMNIA), .attr(AbilityChangeAttr, Abilities.INSOMNIA)
.reflectable(),
new AttackMove(Moves.SUCKER_PUNCH, Type.DARK, MoveCategory.PHYSICAL, 70, 100, 5, -1, 1, 4) new AttackMove(Moves.SUCKER_PUNCH, Type.DARK, MoveCategory.PHYSICAL, 70, 100, 5, -1, 1, 4)
.condition((user, target, move) => globalScene.currentBattle.turnCommands[target.getBattlerIndex()]?.command === Command.FIGHT && !target.turnData.acted && allMoves[globalScene.currentBattle.turnCommands[target.getBattlerIndex()]?.move?.move!].category !== MoveCategory.STATUS), // TODO: is this bang correct? .condition((user, target, move) => globalScene.currentBattle.turnCommands[target.getBattlerIndex()]?.command === Command.FIGHT && !target.turnData.acted && allMoves[globalScene.currentBattle.turnCommands[target.getBattlerIndex()]?.move?.move!].category !== MoveCategory.STATUS), // TODO: is this bang correct?
new StatusMove(Moves.TOXIC_SPIKES, Type.POISON, -1, 20, -1, 0, 4) new StatusMove(Moves.TOXIC_SPIKES, Type.POISON, -1, 20, -1, 0, 4)
.attr(AddArenaTrapTagAttr, ArenaTagType.TOXIC_SPIKES) .attr(AddArenaTrapTagAttr, ArenaTagType.TOXIC_SPIKES)
.target(MoveTarget.ENEMY_SIDE), .target(MoveTarget.ENEMY_SIDE)
.reflectable(),
new StatusMove(Moves.HEART_SWAP, Type.PSYCHIC, -1, 10, -1, 0, 4) new StatusMove(Moves.HEART_SWAP, Type.PSYCHIC, -1, 10, -1, 0, 4)
.attr(SwapStatStagesAttr, BATTLE_STATS) .attr(SwapStatStagesAttr, BATTLE_STATS)
.ignoresSubstitute(), .ignoresSubstitute(),
@ -9529,7 +9604,8 @@ export function initMoves() {
.attr(ClearTerrainAttr) .attr(ClearTerrainAttr)
.attr(RemoveScreensAttr, false) .attr(RemoveScreensAttr, false)
.attr(RemoveArenaTrapAttr, true) .attr(RemoveArenaTrapAttr, true)
.attr(RemoveArenaTagsAttr, [ ArenaTagType.MIST, ArenaTagType.SAFEGUARD ], false), .attr(RemoveArenaTagsAttr, [ ArenaTagType.MIST, ArenaTagType.SAFEGUARD ], false)
.reflectable(),
new StatusMove(Moves.TRICK_ROOM, Type.PSYCHIC, -1, 5, -1, -7, 4) new StatusMove(Moves.TRICK_ROOM, Type.PSYCHIC, -1, 5, -1, -7, 4)
.attr(AddArenaTagAttr, ArenaTagType.TRICK_ROOM, 5) .attr(AddArenaTagAttr, ArenaTagType.TRICK_ROOM, 5)
.ignoresProtect() .ignoresProtect()
@ -9567,10 +9643,12 @@ export function initMoves() {
new StatusMove(Moves.CAPTIVATE, Type.NORMAL, 100, 20, -1, 0, 4) new StatusMove(Moves.CAPTIVATE, Type.NORMAL, 100, 20, -1, 0, 4)
.attr(StatStageChangeAttr, [ Stat.SPATK ], -2) .attr(StatStageChangeAttr, [ Stat.SPATK ], -2)
.condition((user, target, move) => target.isOppositeGender(user)) .condition((user, target, move) => target.isOppositeGender(user))
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES)
.reflectable(),
new StatusMove(Moves.STEALTH_ROCK, Type.ROCK, -1, 20, -1, 0, 4) new StatusMove(Moves.STEALTH_ROCK, Type.ROCK, -1, 20, -1, 0, 4)
.attr(AddArenaTrapTagAttr, ArenaTagType.STEALTH_ROCK) .attr(AddArenaTrapTagAttr, ArenaTagType.STEALTH_ROCK)
.target(MoveTarget.ENEMY_SIDE), .target(MoveTarget.ENEMY_SIDE)
.reflectable(),
new AttackMove(Moves.GRASS_KNOT, Type.GRASS, MoveCategory.SPECIAL, -1, 100, 20, -1, 0, 4) new AttackMove(Moves.GRASS_KNOT, Type.GRASS, MoveCategory.SPECIAL, -1, 100, 20, -1, 0, 4)
.attr(WeightPowerAttr) .attr(WeightPowerAttr)
.makesContact(), .makesContact(),
@ -9614,7 +9692,8 @@ export function initMoves() {
.attr(TrapAttr, BattlerTagType.MAGMA_STORM), .attr(TrapAttr, BattlerTagType.MAGMA_STORM),
new StatusMove(Moves.DARK_VOID, Type.DARK, 80, 10, -1, 0, 4) //Accuracy from Generations 4-6 new StatusMove(Moves.DARK_VOID, Type.DARK, 80, 10, -1, 0, 4) //Accuracy from Generations 4-6
.attr(StatusEffectAttr, StatusEffect.SLEEP) .attr(StatusEffectAttr, StatusEffect.SLEEP)
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES)
.reflectable(),
new AttackMove(Moves.SEED_FLARE, Type.GRASS, MoveCategory.SPECIAL, 120, 85, 5, 40, 0, 4) new AttackMove(Moves.SEED_FLARE, Type.GRASS, MoveCategory.SPECIAL, 120, 85, 5, 40, 0, 4)
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -2), .attr(StatStageChangeAttr, [ Stat.SPDEF ], -2),
new AttackMove(Moves.OMINOUS_WIND, Type.GHOST, MoveCategory.SPECIAL, 60, 100, 5, 10, 0, 4) new AttackMove(Moves.OMINOUS_WIND, Type.GHOST, MoveCategory.SPECIAL, 60, 100, 5, 10, 0, 4)
@ -9654,7 +9733,8 @@ export function initMoves() {
.condition((_user, target, _move) => !(target.species.speciesId === Species.GENGAR && target.getFormKey() === "mega")) .condition((_user, target, _move) => !(target.species.speciesId === Species.GENGAR && target.getFormKey() === "mega"))
.condition((_user, target, _move) => Utils.isNullOrUndefined(target.getTag(BattlerTagType.INGRAIN)) && Utils.isNullOrUndefined(target.getTag(BattlerTagType.IGNORE_FLYING))) .condition((_user, target, _move) => Utils.isNullOrUndefined(target.getTag(BattlerTagType.INGRAIN)) && Utils.isNullOrUndefined(target.getTag(BattlerTagType.IGNORE_FLYING)))
.attr(AddBattlerTagAttr, BattlerTagType.TELEKINESIS, false, true, 3) .attr(AddBattlerTagAttr, BattlerTagType.TELEKINESIS, false, true, 3)
.attr(AddBattlerTagAttr, BattlerTagType.FLOATING, false, true, 3), .attr(AddBattlerTagAttr, BattlerTagType.FLOATING, false, true, 3)
.reflectable(),
new StatusMove(Moves.MAGIC_ROOM, Type.PSYCHIC, -1, 10, -1, 0, 5) new StatusMove(Moves.MAGIC_ROOM, Type.PSYCHIC, -1, 10, -1, 0, 5)
.ignoresProtect() .ignoresProtect()
.target(MoveTarget.BOTH_SIDES) .target(MoveTarget.BOTH_SIDES)
@ -9687,7 +9767,8 @@ export function initMoves() {
.attr(ElectroBallPowerAttr) .attr(ElectroBallPowerAttr)
.ballBombMove(), .ballBombMove(),
new StatusMove(Moves.SOAK, Type.WATER, 100, 20, -1, 0, 5) new StatusMove(Moves.SOAK, Type.WATER, 100, 20, -1, 0, 5)
.attr(ChangeTypeAttr, Type.WATER), .attr(ChangeTypeAttr, Type.WATER)
.reflectable(),
new AttackMove(Moves.FLAME_CHARGE, Type.FIRE, MoveCategory.PHYSICAL, 50, 100, 20, 100, 0, 5) new AttackMove(Moves.FLAME_CHARGE, Type.FIRE, MoveCategory.PHYSICAL, 50, 100, 20, 100, 0, 5)
.attr(StatStageChangeAttr, [ Stat.SPD ], 1, true), .attr(StatStageChangeAttr, [ Stat.SPD ], 1, true),
new SelfStatusMove(Moves.COIL, Type.POISON, -1, 20, -1, 0, 5) new SelfStatusMove(Moves.COIL, Type.POISON, -1, 20, -1, 0, 5)
@ -9700,9 +9781,11 @@ export function initMoves() {
new AttackMove(Moves.FOUL_PLAY, Type.DARK, MoveCategory.PHYSICAL, 95, 100, 15, -1, 0, 5) new AttackMove(Moves.FOUL_PLAY, Type.DARK, MoveCategory.PHYSICAL, 95, 100, 15, -1, 0, 5)
.attr(TargetAtkUserAtkAttr), .attr(TargetAtkUserAtkAttr),
new StatusMove(Moves.SIMPLE_BEAM, Type.NORMAL, 100, 15, -1, 0, 5) new StatusMove(Moves.SIMPLE_BEAM, Type.NORMAL, 100, 15, -1, 0, 5)
.attr(AbilityChangeAttr, Abilities.SIMPLE), .attr(AbilityChangeAttr, Abilities.SIMPLE)
.reflectable(),
new StatusMove(Moves.ENTRAINMENT, Type.NORMAL, 100, 15, -1, 0, 5) new StatusMove(Moves.ENTRAINMENT, Type.NORMAL, 100, 15, -1, 0, 5)
.attr(AbilityGiveAttr), .attr(AbilityGiveAttr)
.reflectable(),
new StatusMove(Moves.AFTER_YOU, Type.NORMAL, -1, 15, -1, 0, 5) new StatusMove(Moves.AFTER_YOU, Type.NORMAL, -1, 15, -1, 0, 5)
.ignoresProtect() .ignoresProtect()
.ignoresSubstitute() .ignoresSubstitute()
@ -9740,7 +9823,8 @@ export function initMoves() {
new StatusMove(Moves.HEAL_PULSE, Type.PSYCHIC, -1, 10, -1, 0, 5) new StatusMove(Moves.HEAL_PULSE, Type.PSYCHIC, -1, 10, -1, 0, 5)
.attr(HealAttr, 0.5, false, false) .attr(HealAttr, 0.5, false, false)
.pulseMove() .pulseMove()
.triageMove(), .triageMove()
.reflectable(),
new AttackMove(Moves.HEX, Type.GHOST, MoveCategory.SPECIAL, 65, 100, 10, -1, 0, 5) new AttackMove(Moves.HEX, Type.GHOST, MoveCategory.SPECIAL, 65, 100, 10, -1, 0, 5)
.attr( .attr(
MovePowerMultiplierAttr, MovePowerMultiplierAttr,
@ -9943,7 +10027,8 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => target.isOfType(Type.GRASS) && target.isGrounded() }), .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => target.isOfType(Type.GRASS) && target.isGrounded() }),
new StatusMove(Moves.STICKY_WEB, Type.BUG, -1, 20, -1, 0, 6) new StatusMove(Moves.STICKY_WEB, Type.BUG, -1, 20, -1, 0, 6)
.attr(AddArenaTrapTagAttr, ArenaTagType.STICKY_WEB) .attr(AddArenaTrapTagAttr, ArenaTagType.STICKY_WEB)
.target(MoveTarget.ENEMY_SIDE), .target(MoveTarget.ENEMY_SIDE)
.reflectable(),
new AttackMove(Moves.FELL_STINGER, Type.BUG, MoveCategory.PHYSICAL, 50, 100, 25, -1, 0, 6) new AttackMove(Moves.FELL_STINGER, Type.BUG, MoveCategory.PHYSICAL, 50, 100, 25, -1, 0, 6)
.attr(PostVictoryStatStageChangeAttr, [ Stat.ATK ], 3, true ), .attr(PostVictoryStatStageChangeAttr, [ Stat.ATK ], 3, true ),
new ChargingAttackMove(Moves.PHANTOM_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6) new ChargingAttackMove(Moves.PHANTOM_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
@ -9951,10 +10036,12 @@ export function initMoves() {
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.HIDDEN) .chargeAttr(SemiInvulnerableAttr, BattlerTagType.HIDDEN)
.ignoresProtect(), .ignoresProtect(),
new StatusMove(Moves.TRICK_OR_TREAT, Type.GHOST, 100, 20, -1, 0, 6) new StatusMove(Moves.TRICK_OR_TREAT, Type.GHOST, 100, 20, -1, 0, 6)
.attr(AddTypeAttr, Type.GHOST), .attr(AddTypeAttr, Type.GHOST)
.reflectable(),
new StatusMove(Moves.NOBLE_ROAR, Type.NORMAL, 100, 30, -1, 0, 6) new StatusMove(Moves.NOBLE_ROAR, Type.NORMAL, 100, 30, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1)
.soundBased(), .soundBased()
.reflectable(),
new StatusMove(Moves.ION_DELUGE, Type.ELECTRIC, -1, 25, -1, 1, 6) new StatusMove(Moves.ION_DELUGE, Type.ELECTRIC, -1, 25, -1, 1, 6)
.attr(AddArenaTagAttr, ArenaTagType.ION_DELUGE) .attr(AddArenaTagAttr, ArenaTagType.ION_DELUGE)
.target(MoveTarget.BOTH_SIDES), .target(MoveTarget.BOTH_SIDES),
@ -9963,7 +10050,8 @@ export function initMoves() {
.target(MoveTarget.ALL_NEAR_OTHERS) .target(MoveTarget.ALL_NEAR_OTHERS)
.triageMove(), .triageMove(),
new StatusMove(Moves.FORESTS_CURSE, Type.GRASS, 100, 20, -1, 0, 6) new StatusMove(Moves.FORESTS_CURSE, Type.GRASS, 100, 20, -1, 0, 6)
.attr(AddTypeAttr, Type.GRASS), .attr(AddTypeAttr, Type.GRASS)
.reflectable(),
new AttackMove(Moves.PETAL_BLIZZARD, Type.GRASS, MoveCategory.PHYSICAL, 90, 100, 15, -1, 0, 6) new AttackMove(Moves.PETAL_BLIZZARD, Type.GRASS, MoveCategory.PHYSICAL, 90, 100, 15, -1, 0, 6)
.windMove() .windMove()
.makesContact(false) .makesContact(false)
@ -9977,9 +10065,11 @@ export function initMoves() {
new StatusMove(Moves.PARTING_SHOT, Type.DARK, 100, 20, -1, 0, 6) new StatusMove(Moves.PARTING_SHOT, Type.DARK, 100, 20, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, false, { trigger: MoveEffectTrigger.PRE_APPLY }) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, false, { trigger: MoveEffectTrigger.PRE_APPLY })
.attr(ForceSwitchOutAttr, true) .attr(ForceSwitchOutAttr, true)
.soundBased(), .soundBased()
.reflectable(),
new StatusMove(Moves.TOPSY_TURVY, Type.DARK, -1, 20, -1, 0, 6) new StatusMove(Moves.TOPSY_TURVY, Type.DARK, -1, 20, -1, 0, 6)
.attr(InvertStatsAttr), .attr(InvertStatsAttr)
.reflectable(),
new AttackMove(Moves.DRAINING_KISS, Type.FAIRY, MoveCategory.SPECIAL, 50, 100, 10, -1, 0, 6) new AttackMove(Moves.DRAINING_KISS, Type.FAIRY, MoveCategory.SPECIAL, 50, 100, 10, -1, 0, 6)
.attr(HitHealAttr, 0.75) .attr(HitHealAttr, 0.75)
.makesContact() .makesContact()
@ -10018,10 +10108,12 @@ export function initMoves() {
.condition(failIfLastCondition), .condition(failIfLastCondition),
new StatusMove(Moves.PLAY_NICE, Type.NORMAL, -1, 20, -1, 0, 6) new StatusMove(Moves.PLAY_NICE, Type.NORMAL, -1, 20, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.ATK ], -1) .attr(StatStageChangeAttr, [ Stat.ATK ], -1)
.ignoresSubstitute(), .ignoresSubstitute()
.reflectable(),
new StatusMove(Moves.CONFIDE, Type.NORMAL, -1, 20, -1, 0, 6) new StatusMove(Moves.CONFIDE, Type.NORMAL, -1, 20, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.SPATK ], -1) .attr(StatStageChangeAttr, [ Stat.SPATK ], -1)
.soundBased(), .soundBased()
.reflectable(),
new AttackMove(Moves.DIAMOND_STORM, Type.ROCK, MoveCategory.PHYSICAL, 100, 95, 5, 50, 0, 6) new AttackMove(Moves.DIAMOND_STORM, Type.ROCK, MoveCategory.PHYSICAL, 100, 95, 5, 50, 0, 6)
.attr(StatStageChangeAttr, [ Stat.DEF ], 2, true, { firstTargetOnly: true }) .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true, { firstTargetOnly: true })
.makesContact(false) .makesContact(false)
@ -10048,14 +10140,17 @@ export function initMoves() {
.condition(failIfSingleBattle) .condition(failIfSingleBattle)
.target(MoveTarget.NEAR_ALLY), .target(MoveTarget.NEAR_ALLY),
new StatusMove(Moves.EERIE_IMPULSE, Type.ELECTRIC, 100, 15, -1, 0, 6) new StatusMove(Moves.EERIE_IMPULSE, Type.ELECTRIC, 100, 15, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.SPATK ], -2), .attr(StatStageChangeAttr, [ Stat.SPATK ], -2)
.reflectable(),
new StatusMove(Moves.VENOM_DRENCH, Type.POISON, 100, 20, -1, 0, 6) new StatusMove(Moves.VENOM_DRENCH, Type.POISON, 100, 20, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK, Stat.SPD ], -1, false, { condition: (user, target, move) => target.status?.effect === StatusEffect.POISON || target.status?.effect === StatusEffect.TOXIC }) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK, Stat.SPD ], -1, false, { condition: (user, target, move) => target.status?.effect === StatusEffect.POISON || target.status?.effect === StatusEffect.TOXIC })
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES)
.reflectable(),
new StatusMove(Moves.POWDER, Type.BUG, 100, 20, -1, 1, 6) new StatusMove(Moves.POWDER, Type.BUG, 100, 20, -1, 1, 6)
.attr(AddBattlerTagAttr, BattlerTagType.POWDER, false, true) .attr(AddBattlerTagAttr, BattlerTagType.POWDER, false, true)
.ignoresSubstitute() .ignoresSubstitute()
.powderMove(), .powderMove()
.reflectable(),
new ChargingSelfStatusMove(Moves.GEOMANCY, Type.FAIRY, -1, 10, -1, 0, 6) new ChargingSelfStatusMove(Moves.GEOMANCY, Type.FAIRY, -1, 10, -1, 0, 6)
.chargeText(i18next.t("moveTriggers:isChargingPower", { pokemonName: "{USER}" })) .chargeText(i18next.t("moveTriggers:isChargingPower", { pokemonName: "{USER}" }))
.attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true), .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true),
@ -10077,7 +10172,8 @@ export function initMoves() {
.ignoresSubstitute() .ignoresSubstitute()
.target(MoveTarget.NEAR_ALLY), .target(MoveTarget.NEAR_ALLY),
new StatusMove(Moves.BABY_DOLL_EYES, Type.FAIRY, 100, 30, -1, 1, 6) new StatusMove(Moves.BABY_DOLL_EYES, Type.FAIRY, 100, 30, -1, 1, 6)
.attr(StatStageChangeAttr, [ Stat.ATK ], -1), .attr(StatStageChangeAttr, [ Stat.ATK ], -1)
.reflectable(),
new AttackMove(Moves.NUZZLE, Type.ELECTRIC, MoveCategory.PHYSICAL, 20, 100, 20, 100, 0, 6) new AttackMove(Moves.NUZZLE, Type.ELECTRIC, MoveCategory.PHYSICAL, 20, 100, 20, 100, 0, 6)
.attr(StatusEffectAttr, StatusEffect.PARALYSIS), .attr(StatusEffectAttr, StatusEffect.PARALYSIS),
new AttackMove(Moves.HOLD_BACK, Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 40, -1, 0, 6) new AttackMove(Moves.HOLD_BACK, Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 40, -1, 0, 6)
@ -10221,13 +10317,15 @@ export function initMoves() {
.punchingMove(), .punchingMove(),
new StatusMove(Moves.FLORAL_HEALING, Type.FAIRY, -1, 10, -1, 0, 7) new StatusMove(Moves.FLORAL_HEALING, Type.FAIRY, -1, 10, -1, 0, 7)
.attr(BoostHealAttr, 0.5, 2 / 3, true, false, (user, target, move) => globalScene.arena.terrain?.terrainType === TerrainType.GRASSY) .attr(BoostHealAttr, 0.5, 2 / 3, true, false, (user, target, move) => globalScene.arena.terrain?.terrainType === TerrainType.GRASSY)
.triageMove(), .triageMove()
.reflectable(),
new AttackMove(Moves.HIGH_HORSEPOWER, Type.GROUND, MoveCategory.PHYSICAL, 95, 95, 10, -1, 0, 7), new AttackMove(Moves.HIGH_HORSEPOWER, Type.GROUND, MoveCategory.PHYSICAL, 95, 95, 10, -1, 0, 7),
new StatusMove(Moves.STRENGTH_SAP, Type.GRASS, 100, 10, -1, 0, 7) new StatusMove(Moves.STRENGTH_SAP, Type.GRASS, 100, 10, -1, 0, 7)
.attr(HitHealAttr, null, Stat.ATK) .attr(HitHealAttr, null, Stat.ATK)
.attr(StatStageChangeAttr, [ Stat.ATK ], -1) .attr(StatStageChangeAttr, [ Stat.ATK ], -1)
.condition((user, target, move) => target.getStatStage(Stat.ATK) > -6) .condition((user, target, move) => target.getStatStage(Stat.ATK) > -6)
.triageMove(), .triageMove()
.reflectable(),
new ChargingAttackMove(Moves.SOLAR_BLADE, Type.GRASS, MoveCategory.PHYSICAL, 125, 100, 10, -1, 0, 7) new ChargingAttackMove(Moves.SOLAR_BLADE, Type.GRASS, MoveCategory.PHYSICAL, 125, 100, 10, -1, 0, 7)
.chargeText(i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" })) .chargeText(i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" }))
.chargeAttr(WeatherInstantChargeAttr, [ WeatherType.SUNNY, WeatherType.HARSH_SUN ]) .chargeAttr(WeatherInstantChargeAttr, [ WeatherType.SUNNY, WeatherType.HARSH_SUN ])
@ -10237,10 +10335,12 @@ export function initMoves() {
.makesContact(false), .makesContact(false),
new StatusMove(Moves.SPOTLIGHT, Type.NORMAL, -1, 15, -1, 3, 7) new StatusMove(Moves.SPOTLIGHT, Type.NORMAL, -1, 15, -1, 3, 7)
.attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, false) .attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, false)
.condition(failIfSingleBattle), .condition(failIfSingleBattle)
.reflectable(),
new StatusMove(Moves.TOXIC_THREAD, Type.POISON, 100, 20, -1, 0, 7) new StatusMove(Moves.TOXIC_THREAD, Type.POISON, 100, 20, -1, 0, 7)
.attr(StatusEffectAttr, StatusEffect.POISON) .attr(StatusEffectAttr, StatusEffect.POISON)
.attr(StatStageChangeAttr, [ Stat.SPD ], -1), .attr(StatStageChangeAttr, [ Stat.SPD ], -1)
.reflectable(),
new SelfStatusMove(Moves.LASER_FOCUS, Type.NORMAL, -1, 30, -1, 0, 7) new SelfStatusMove(Moves.LASER_FOCUS, Type.NORMAL, -1, 30, -1, 0, 7)
.attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false), .attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false),
new StatusMove(Moves.GEAR_UP, Type.STEEL, -1, 20, -1, 0, 7) new StatusMove(Moves.GEAR_UP, Type.STEEL, -1, 20, -1, 0, 7)
@ -10284,7 +10384,8 @@ export function initMoves() {
(user: Pokemon, target: Pokemon, move: Move) => isNonVolatileStatusEffect(target.status?.effect!)) // TODO: is this bang correct? (user: Pokemon, target: Pokemon, move: Move) => isNonVolatileStatusEffect(target.status?.effect!)) // TODO: is this bang correct?
.attr(HealAttr, 0.5) .attr(HealAttr, 0.5)
.attr(HealStatusEffectAttr, false, getNonVolatileStatusEffects()) .attr(HealStatusEffectAttr, false, getNonVolatileStatusEffects())
.triageMove(), .triageMove()
.reflectable(),
new AttackMove(Moves.REVELATION_DANCE, Type.NORMAL, MoveCategory.SPECIAL, 90, 100, 15, -1, 0, 7) new AttackMove(Moves.REVELATION_DANCE, Type.NORMAL, MoveCategory.SPECIAL, 90, 100, 15, -1, 0, 7)
.danceMove() .danceMove()
.attr(MatchUserTypeAttr), .attr(MatchUserTypeAttr),
@ -10373,7 +10474,8 @@ export function initMoves() {
new AttackMove(Moves.MOONGEIST_BEAM, Type.GHOST, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 7) new AttackMove(Moves.MOONGEIST_BEAM, Type.GHOST, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 7)
.ignoresAbilities(), .ignoresAbilities(),
new StatusMove(Moves.TEARFUL_LOOK, Type.NORMAL, -1, 20, -1, 0, 7) new StatusMove(Moves.TEARFUL_LOOK, Type.NORMAL, -1, 20, -1, 0, 7)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1), .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1)
.reflectable(),
new AttackMove(Moves.ZING_ZAP, Type.ELECTRIC, MoveCategory.PHYSICAL, 80, 100, 10, 30, 0, 7) new AttackMove(Moves.ZING_ZAP, Type.ELECTRIC, MoveCategory.PHYSICAL, 80, 100, 10, 30, 0, 7)
.attr(FlinchAttr), .attr(FlinchAttr),
new AttackMove(Moves.NATURES_MADNESS, Type.FAIRY, MoveCategory.SPECIAL, -1, 90, 10, -1, 0, 7) new AttackMove(Moves.NATURES_MADNESS, Type.FAIRY, MoveCategory.SPECIAL, -1, 90, 10, -1, 0, 7)
@ -10492,10 +10594,12 @@ export function initMoves() {
.condition((user, target, move) => user.getTag(TrappedTag)?.sourceMove !== Moves.NO_RETREAT), // fails if the user is currently trapped by No Retreat .condition((user, target, move) => user.getTag(TrappedTag)?.sourceMove !== Moves.NO_RETREAT), // fails if the user is currently trapped by No Retreat
new StatusMove(Moves.TAR_SHOT, Type.ROCK, 100, 15, -1, 0, 8) new StatusMove(Moves.TAR_SHOT, Type.ROCK, 100, 15, -1, 0, 8)
.attr(StatStageChangeAttr, [ Stat.SPD ], -1) .attr(StatStageChangeAttr, [ Stat.SPD ], -1)
.attr(AddBattlerTagAttr, BattlerTagType.TAR_SHOT, false), .attr(AddBattlerTagAttr, BattlerTagType.TAR_SHOT, false)
.reflectable(),
new StatusMove(Moves.MAGIC_POWDER, Type.PSYCHIC, 100, 20, -1, 0, 8) new StatusMove(Moves.MAGIC_POWDER, Type.PSYCHIC, 100, 20, -1, 0, 8)
.attr(ChangeTypeAttr, Type.PSYCHIC) .attr(ChangeTypeAttr, Type.PSYCHIC)
.powderMove(), .powderMove()
.reflectable(),
new AttackMove(Moves.DRAGON_DARTS, Type.DRAGON, MoveCategory.PHYSICAL, 50, 100, 10, -1, 0, 8) new AttackMove(Moves.DRAGON_DARTS, Type.DRAGON, MoveCategory.PHYSICAL, 50, 100, 10, -1, 0, 8)
.attr(MultiHitAttr, MultiHitType._2) .attr(MultiHitAttr, MultiHitType._2)
.makesContact(false) .makesContact(false)
@ -10672,6 +10776,7 @@ export function initMoves() {
.makesContact(false), .makesContact(false),
new StatusMove(Moves.CORROSIVE_GAS, Type.POISON, 100, 40, -1, 0, 8) new StatusMove(Moves.CORROSIVE_GAS, Type.POISON, 100, 40, -1, 0, 8)
.target(MoveTarget.ALL_NEAR_OTHERS) .target(MoveTarget.ALL_NEAR_OTHERS)
.reflectable()
.unimplemented(), .unimplemented(),
new StatusMove(Moves.COACHING, Type.FIGHTING, -1, 10, -1, 0, 8) new StatusMove(Moves.COACHING, Type.FIGHTING, -1, 10, -1, 0, 8)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], 1) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], 1)

View File

@ -94,4 +94,5 @@ export enum BattlerTagType {
PSYCHO_SHIFT = "PSYCHO_SHIFT", PSYCHO_SHIFT = "PSYCHO_SHIFT",
ENDURE_TOKEN = "ENDURE_TOKEN", ENDURE_TOKEN = "ENDURE_TOKEN",
POWDER = "POWDER", POWDER = "POWDER",
MAGIC_COAT = "MAGIC_COAT",
} }

View File

@ -12,6 +12,7 @@ import {
PostAttackAbAttr, PostAttackAbAttr,
PostDamageAbAttr, PostDamageAbAttr,
PostDefendAbAttr, PostDefendAbAttr,
ReflectStatusMoveAbAttr,
TypeImmunityAbAttr, TypeImmunityAbAttr,
} from "#app/data/ability"; } from "#app/data/ability";
import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag"; import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag";
@ -31,6 +32,7 @@ import {
AttackMove, AttackMove,
DelayedAttackAttr, DelayedAttackAttr,
FlinchAttr, FlinchAttr,
getMoveTargets,
HitsTagAttr, HitsTagAttr,
MissEffectAttr, MissEffectAttr,
MoveCategory, MoveCategory,
@ -47,7 +49,7 @@ import {
} from "#app/data/move"; } from "#app/data/move";
import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms"; import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms";
import { Type } from "#enums/type"; 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 type Pokemon from "#app/field/pokemon";
import { HitResult, MoveResult } from "#app/field/pokemon"; import { HitResult, MoveResult } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
@ -60,17 +62,27 @@ import {
} from "#app/modifier/modifier"; } from "#app/modifier/modifier";
import { PokemonPhase } from "#app/phases/pokemon-phase"; import { PokemonPhase } from "#app/phases/pokemon-phase";
import { BooleanHolder, executeIf, isNullOrUndefined, NumberHolder } from "#app/utils"; import { BooleanHolder, executeIf, isNullOrUndefined, NumberHolder } from "#app/utils";
import { type nil } from "#app/utils";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import type { Moves } from "#enums/moves"; import type { Moves } from "#enums/moves";
import i18next from "i18next"; import i18next from "i18next";
import type { Phase } from "#app/phase";
import { ShowAbilityPhase } from "./show-ability-phase";
import { MovePhase } from "./move-phase";
import { MoveEndPhase } from "./move-end-phase";
export class MoveEffectPhase extends PokemonPhase { export class MoveEffectPhase extends PokemonPhase {
public move: PokemonMove; public move: PokemonMove;
protected targets: BattlerIndex[]; 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); super(battlerIndex);
this.move = move; this.move = move;
this.reflected = reflected;
/** /**
* In double battles, if the right Pokemon selects a spread move and the left Pokemon dies * 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 * with no party members available to switch in, then the right Pokemon takes the index
@ -184,12 +196,14 @@ export class MoveEffectPhase extends PokemonPhase {
&& (targets[0]?.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) && (targets[0]?.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
&& !targets[0]?.getTag(SemiInvulnerableTag); && !targets[0]?.getTag(SemiInvulnerableTag);
const mayBounce = move.hasFlag(MoveFlags.REFLECTABLE) && !this.reflected && targets.some(t => t.hasAbilityWithAttr(ReflectStatusMoveAbAttr) || !!t.getTag(BattlerTagType.MAGIC_COAT));
/** /**
* 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 * (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. * 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(); this.stopMultiHit();
if (hasActiveTargets) { if (hasActiveTargets) {
globalScene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getFirstTarget() ? getPokemonNameWithAffix(this.getFirstTarget()!) : "" })); globalScene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getFirstTarget() ? getPokemonNameWithAffix(this.getFirstTarget()!) : "" }));
@ -211,12 +225,21 @@ export class MoveEffectPhase extends PokemonPhase {
new MoveAnim(move.id as Moves, user, this.getFirstTarget()!.getBattlerIndex(), playOnEmptyField).play(move.hitsSubstitute(user, this.getFirstTarget()!), () => { 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? */ /** Has the move successfully hit a target (for damage) yet? */
let hasHit: boolean = false; 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 check which target will magic bounce.
const trueTargets: Pokemon[] = move.moveTarget !== MoveTarget.ENEMY_SIDE ? targets : (() => {
const magicCoatTargets = targets.filter(t => t.getTag(BattlerTagType.MAGIC_COAT) || t.hasAbilityWithAttr(ReflectStatusMoveAbAttr));
// only magic coat effect cares about order
if (!mayBounce || magicCoatTargets.length === 0) {
return [ targets[0] ];
}
return [ magicCoatTargets[0] ];
})();
const queuedPhases: Phase[] = [];
for (const target of trueTargets) {
/** The {@linkcode ArenaTagSide} to which the target belongs */ /** The {@linkcode ArenaTagSide} to which the target belongs */
const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
/** Has the invoked move been cancelled by conditional protection (e.g Quick Guard)? */ /** Has the invoked move been cancelled by conditional protection (e.g Quick Guard)? */
@ -229,7 +252,7 @@ export class MoveEffectPhase extends PokemonPhase {
} }
/** Is the target protected by Protect, etc. or a relevant conditional protection effect? */ /** Is the target protected by Protect, etc. or a relevant conditional protection effect? */
const isProtected = ( const isProtected = !([ MoveTarget.ENEMY_SIDE, MoveTarget.BOTH_SIDES ].includes(this.move.getMove().moveTarget)) && (
bypassIgnoreProtect.value bypassIgnoreProtect.value
|| !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target)) || !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target))
&& (hasConditionalProtectApplied.value && (hasConditionalProtectApplied.value
@ -238,13 +261,39 @@ export class MoveEffectPhase extends PokemonPhase {
|| (this.move.getMove().category !== MoveCategory.STATUS || (this.move.getMove().category !== MoveCategory.STATUS
&& target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType)))); && 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);
/** 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);
const semiInvulnerableTag = target.getTag(SemiInvulnerableTag);
/** Is the target reflecting the effect, not protected, and not in an semi-invulnerable state?*/
const willBounce = (!isProtected && !this.reflected && !isCommanding
&& move.hasFlag(MoveFlags.REFLECTABLE)
&& (isReflecting || canMagicBounce)
&& !semiInvulnerableTag);
// 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) {
queuedPhases.push(new ShowAbilityPhase(target.getBattlerIndex(), target.getPassiveAbility().hasAttr(ReflectStatusMoveAbAttr)));
}
queuedPhases.push(new MovePhase(target, newTargets, new PokemonMove(move.id, 0, 0, true), true, true, true));
continue;
}
/** Is the pokemon immune due to an ablility, and also not in a semi invulnerable state? */ /** Is the pokemon immune due to an ablility, and also not in a semi invulnerable state? */
const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr) const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr)
&& (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) && (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
&& !target.getTag(SemiInvulnerableTag); && !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 * If the move missed a target, stop all future hits against that target
@ -371,6 +420,10 @@ export class MoveEffectPhase extends PokemonPhase {
applyAttrs.push(k); applyAttrs.push(k);
} }
// Apply queued phases
if (queuedPhases.length) {
globalScene.appendToPhase(queuedPhases, MoveEndPhase);
}
// Apply the move's POST_TARGET effects on the move's last hit, after all targeted effects have resolved // 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()) ? const postTarget = (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive()) ?
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET, user, null, move) : applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET, user, null, move) :
@ -586,12 +639,7 @@ export class MoveEffectPhase extends PokemonPhase {
} }
} }
if (user.hasAbilityWithAttr(AlwaysHitAbAttr) || target.hasAbilityWithAttr(AlwaysHitAbAttr)) { if (this.checkBypassAccAndInvuln(target)) {
return true;
}
// If the user should ignore accuracy on a target, check who the user targeted last turn and see if they match
if (user.getTag(BattlerTagType.IGNORE_ACCURACY) && (user.getLastXMoves().find(() => true)?.targets || []).indexOf(target.getBattlerIndex()) !== -1) {
return true; return true;
} }
@ -599,15 +647,12 @@ export class MoveEffectPhase extends PokemonPhase {
return true; return true;
} }
if (target.getTag(BattlerTagType.TELEKINESIS) && !target.getTag(SemiInvulnerableTag) && !this.move.getMove().hasAttr(OneHitKOAttr)) { const semiInvulnerableTag = target.getTag(SemiInvulnerableTag);
if (target.getTag(BattlerTagType.TELEKINESIS) && !semiInvulnerableTag && !this.move.getMove().hasAttr(OneHitKOAttr)) {
return true; return true;
} }
const semiInvulnerableTag = target.getTag(SemiInvulnerableTag); if (semiInvulnerableTag && !this.checkBypassSemiInvuln(semiInvulnerableTag)) {
if (semiInvulnerableTag
&& !this.move.getMove().getAttrs(HitsTagAttr).some(hta => hta.tagType === semiInvulnerableTag.tagType)
&& !(this.move.getMove().hasAttr(ToxicAccuracyAttr) && user.isOfType(Type.POISON))
) {
return false; return false;
} }
@ -623,6 +668,52 @@ export class MoveEffectPhase extends PokemonPhase {
return rand < (moveAccuracy * accuracyMultiplier); return rand < (moveAccuracy * accuracyMultiplier);
} }
/**
* Check whether the move should bypass *both* the accuracy *and* semi-invulnerable states.
* @param target - The {@linkcode Pokemon} targeted by the invoked move
* @returns `true` if the move should bypass accuracy and semi-invulnerability
*
* Accuracy and semi-invulnerability can be bypassed by:
* - An ability like {@linkcode Abilities.NO_GUARD | No Guard}
* - A poison type using {@linkcode Moves.TOXIC | Toxic}
* - A move like {@linkcode Moves.LOCK_ON | Lock-On} or {@linkcode Moves.MIND_READER | Mind Reader}.
*
* Does *not* check against effects {@linkcode Moves.GLAIVE_RUSH | Glaive Rush} status (which
* should not bypass semi-invulnerability), or interactions like Earthquake hitting against Dig,
* (which should not bypass the accuracy check).
*
* @see {@linkcode hitCheck}
*/
public checkBypassAccAndInvuln(target: Pokemon) {
const user = this.getUserPokemon();
if (!user) {
return false;
}
if (user.hasAbilityWithAttr(AlwaysHitAbAttr) || target.hasAbilityWithAttr(AlwaysHitAbAttr)) {
return true;
}
if ((this.move.getMove().hasAttr(ToxicAccuracyAttr) && user.isOfType(Type.POISON))) {
return true;
}
// TODO: Fix lock on / mind reader check.
if (user.getTag(BattlerTagType.IGNORE_ACCURACY) && (user.getLastXMoves().find(() => true)?.targets || []).indexOf(target.getBattlerIndex()) !== -1) {
return true;
}
}
/**
* Check whether the move is able to ignore the given `semiInvulnerableTag`
* @param semiInvulnerableTag - The semiInvulnerbale tag to check against
* @returns `true` if the move can ignore the semi-invulnerable state
*/
public checkBypassSemiInvuln(semiInvulnerableTag: SemiInvulnerableTag | nil): boolean {
if (!semiInvulnerableTag) {
return false;
}
const move = this.move.getMove();
return move.getAttrs(HitsTagAttr).some(hta => hta.tagType === semiInvulnerableTag.tagType);
}
/** @returns The {@linkcode Pokemon} using this phase's invoked move */ /** @returns The {@linkcode Pokemon} using this phase's invoked move */
public getUserPokemon(): Pokemon | null { public getUserPokemon(): Pokemon | null {
if (this.battlerIndex > BattlerIndex.ENEMY_2) { if (this.battlerIndex > BattlerIndex.ENEMY_2) {

View File

@ -58,6 +58,7 @@ export class MovePhase extends BattlePhase {
protected ignorePp: boolean; protected ignorePp: boolean;
protected failed: boolean = false; protected failed: boolean = false;
protected cancelled: boolean = false; protected cancelled: boolean = false;
protected reflected: boolean = false;
public get pokemon(): Pokemon { public get pokemon(): Pokemon {
return this._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. * 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(); super();
this.pokemon = pokemon; this.pokemon = pokemon;
@ -95,6 +98,7 @@ export class MovePhase extends BattlePhase {
this.move = move; this.move = move;
this.followUp = followUp; this.followUp = followUp;
this.ignorePp = ignorePp; 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. // 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)) { if (this.move.getMove().checkFlag(MoveFlags.IGNORE_ABILITIES, this.pokemon, null)) {
globalScene.arena.setIgnoreAbilities(true, this.pokemon.getBattlerIndex()); globalScene.arena.setIgnoreAbilities(true, this.pokemon.getBattlerIndex());
} }
@ -335,7 +339,7 @@ export class MovePhase extends BattlePhase {
*/ */
if (success) { if (success) {
applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, this.move.getMove()); 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 { } else {
if ([ Moves.ROAR, Moves.WHIRLWIND, Moves.TRICK_OR_TREAT, Moves.FORESTS_CURSE ].includes(this.move.moveId)) { if ([ Moves.ROAR, Moves.WHIRLWIND, Moves.TRICK_OR_TREAT, Moves.FORESTS_CURSE ].includes(this.move.moveId)) {
@ -543,7 +547,7 @@ export class MovePhase extends BattlePhase {
return; return;
} }
globalScene.queueMessage(i18next.t("battle:useMove", { globalScene.queueMessage(i18next.t(this.reflected ? "battle:magicCoatActivated" : "battle:useMove", {
pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon), pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon),
moveName: this.move.getName() moveName: this.move.getName()
}), 500); }), 500);

View File

@ -0,0 +1,351 @@
import { BattlerIndex } from "#app/battle";
import { allAbilities } from "#app/data/ability";
import { ArenaTagSide } from "#app/data/arena-tag";
import { allMoves } from "#app/data/move";
import { ArenaTagType } from "#app/enums/arena-tag-type";
import { BattlerTagType } from "#app/enums/battler-tag-type";
import { Stat } from "#app/enums/stat";
import { StatusEffect } from "#app/enums/status-effect";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
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(Abilities.BALL_FETCH)
.battleType("single")
.moveset( [ Moves.GROWL, Moves.SPLASH ])
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.MAGIC_BOUNCE)
.enemyMoveset(Moves.SPLASH);
});
it("should reflect basic status moves", async () => {
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.move.select(Moves.GROWL);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.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([ Species.MAGIKARP ]);
game.override.moveset([ Moves.GROWL ]);
game.override.enemyMoveset( [ Moves.FLY ]);
game.move.select(Moves.GROWL);
await game.forceEnemyMove(Moves.FLY);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(0);
});
it("should individually bounce back multi-target moves", async () => {
game.override.battleType("double");
game.override.moveset([ Moves.GROWL, Moves.SPLASH ]);
await game.classicMode.startBattle([ Species.MAGIKARP, Species.MAGIKARP ]);
game.move.select(Moves.GROWL, 0);
game.move.select(Moves.SPLASH, 1);
await game.phaseInterceptor.to("BerryPhase");
const user = game.scene.getPlayerField()[0];
expect(user.getStatStage(Stat.ATK)).toBe(-2);
});
it("should still bounce back a move that would otherwise fail", async () => {
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.scene.getEnemyPokemon()?.setStatStage(Stat.ATK, -6);
game.override.moveset([ Moves.GROWL ]);
game.move.select(Moves.GROWL);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(-1);
});
it("should not bounce back a move that was just bounced", async () => {
game.override.ability(Abilities.MAGIC_BOUNCE);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.move.select(Moves.GROWL);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.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(Abilities.MIRROR_ARMOR);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.move.select(Moves.GROWL);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(-1);
});
it("should not bounce back a move from a mold breaker user", async () => {
game.override.ability(Abilities.MOLD_BREAKER);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.move.select(Moves.GROWL);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(-1);
});
it("should bounce back a spread status move against both pokemon", async () => {
game.override.battleType("double");
game.override.moveset([ Moves.GROWL, Moves.SPLASH ]);
game.override.enemyMoveset([ Moves.SPLASH ]);
await game.classicMode.startBattle([ Species.MAGIKARP, Species.MAGIKARP ]);
game.move.select(Moves.GROWL, 0);
game.move.select(Moves.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.battleType("double");
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.override.moveset([ Moves.SPIKES ]);
game.move.select(Moves.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([ Moves.SPIKES ]);
game.override.enemyMoveset([ Moves.PROTECT ]);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.move.select(Moves.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([ Moves.SPIKES ]);
game.override.enemyMoveset([ Moves.FLY ]);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.move.select(Moves.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.starterSpecies(Species.GASTLY);
await game.classicMode.startBattle([ Species.GASTLY ]);
game.override.moveset([ Moves.CURSE ]);
game.move.select(Moves.CURSE);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getEnemyPokemon()!.getTag(BattlerTagType.CURSED)).toBeDefined();
});
it("should not cause encore to be interrupted after bouncing", async () => {
game.override.moveset([ Moves.SPLASH, Moves.GROWL, Moves.ENCORE ]);
game.override.enemyMoveset([ Moves.TACKLE, Moves.GROWL ]);
// game.override.ability(Abilities.MOLD_BREAKER);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
// Give the player MOLD_BREAKER for this turn to bypass Magic Bounce.
vi.spyOn(playerPokemon, "getAbility").mockReturnValue(allAbilities[Abilities.MOLD_BREAKER]);
// turn 1
game.move.select(Moves.ENCORE);
await game.forceEnemyMove(Moves.TACKLE);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.toNextTurn();
expect(enemyPokemon.getTag(BattlerTagType.ENCORE)!["moveId"]).toBe(Moves.TACKLE);
// turn 2
vi.spyOn(playerPokemon, "getAbility").mockRestore();
game.move.select(Moves.GROWL);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon.getTag(BattlerTagType.ENCORE)!["moveId"]).toBe(Moves.TACKLE);
expect(enemyPokemon.getLastXMoves()[0].move).toBe(Moves.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([ Moves.SPLASH, Moves.GROWL, Moves.ENCORE ]);
game.override.enemyMoveset([ Moves.GROWL, Moves.TACKLE ]);
game.override.enemyAbility(Abilities.MAGIC_BOUNCE);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
// turn 1
game.move.select(Moves.GROWL);
await game.forceEnemyMove(Moves.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[Abilities.MOLD_BREAKER]);
// turn 2
game.move.select(Moves.ENCORE);
await game.forceEnemyMove(Moves.TACKLE);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon.getTag(BattlerTagType.ENCORE)!["moveId"]).toBe(Moves.TACKLE);
expect(enemyPokemon.getLastXMoves()[0].move).toBe(Moves.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.battleType("single");
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.override.moveset([ Moves.STOMPING_TANTRUM, Moves.CHARM ]);
const stomping_tantrum = allMoves[Moves.STOMPING_TANTRUM];
vi.spyOn(stomping_tantrum, "calculateBattlePower");
game.move.select(Moves.CHARM);
await game.toNextTurn();
game.move.select(Moves.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([ Moves.STOMPING_TANTRUM, Moves.SPLASH, Moves.CHARM ]);
await game.classicMode.startBattle([ Species.BULBASAUR ]);
const stomping_tantrum = allMoves[Moves.STOMPING_TANTRUM];
const enemy = game.scene.getEnemyPokemon()!;
vi.spyOn(stomping_tantrum, "calculateBattlePower");
game.move.select(Moves.SPORE);
await game.forceEnemyMove(Moves.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(Moves.GROWL);
await game.phaseInterceptor.to("BerryPhase");
expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(75);
});
it("should respect immunities when bouncing a move", async () => {
vi.spyOn(allMoves[Moves.THUNDER_WAVE], "accuracy", "get").mockReturnValue(100);
game.override.moveset([ Moves.THUNDER_WAVE, Moves.GROWL ]);
game.override.ability(Abilities.SOUNDPROOF);
await game.classicMode.startBattle([ Species.PHANPY ]);
// Turn 1 - thunder wave immunity test
game.move.select(Moves.THUNDER_WAVE);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()!.status).toBeUndefined();
// Turn 2 - soundproof immunity test
game.move.select(Moves.GROWL);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(0);
});
it("should bounce back a move before the accuracy check", async () => {
game.override.moveset([ Moves.SPORE ]);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const attacker = game.scene.getPlayerPokemon()!;
vi.spyOn(attacker, "getAccuracyMultiplier").mockReturnValue(0.0);
game.move.select(Moves.SPORE);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()!.status?.effect).toBe(StatusEffect.SLEEP);
});
it("should take the accuracy of the magic bounce user into account", async () => {
game.override.moveset([ Moves.SPORE ]);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const opponent = game.scene.getEnemyPokemon()!;
vi.spyOn(opponent, "getAccuracyMultiplier").mockReturnValue(0);
game.move.select(Moves.SPORE);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.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.battleType("double");
game.override.moveset([ Moves.STICKY_WEB, Moves.SPLASH, Moves.TRICK_ROOM ]);
await game.classicMode.startBattle([ Species.MAGIKARP, Species.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(Moves.STICKY_WEB, 0);
game.move.select(Moves.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(Moves.STICKY_WEB, 0);
game.move.select(Moves.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([ Moves.TOXIC, Moves.CHARM ]);
await game.classicMode.startBattle([ Species.BULBASAUR ]);
game.move.select(Moves.TOXIC);
await game.forceEnemyMove(Moves.FLY);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getEnemyPokemon()!.status?.effect).toBe(StatusEffect.TOXIC);
expect(game.scene.getPlayerPokemon()!.status).toBeUndefined();
game.override.ability(Abilities.NO_GUARD);
game.move.select(Moves.CHARM);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(-2);
expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(0);
});
});

View File

@ -0,0 +1,286 @@
import { BattlerIndex } from "#app/battle";
import { ArenaTagSide } from "#app/data/arena-tag";
import { allMoves } from "#app/data/move";
import { ArenaTagType } from "#app/enums/arena-tag-type";
import { BattlerTagType } from "#app/enums/battler-tag-type";
import { Stat } from "#app/enums/stat";
import { StatusEffect } from "#app/enums/status-effect";
import { MoveResult } from "#app/field/pokemon";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
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(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.MAGIC_COAT);
});
it("should fail if the user goes last in the turn", async () => {
game.override.moveset([ Moves.PROTECT ]);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.move.select(Moves.PROTECT);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.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([ Moves.INSTRUCT ]);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.move.select(Moves.INSTRUCT);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getEnemyPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
});
it("should not reflect moves used on the next turn", async () => {
game.override.moveset([ Moves.GROWL, Moves.SPLASH ]);
game.override.enemyMoveset([ Moves.MAGIC_COAT, Moves.SPLASH ]);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
// turn 1
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.MAGIC_COAT);
await game.toNextTurn();
// turn 2
game.move.select(Moves.GROWL);
await game.forceEnemyMove(Moves.SPLASH);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(-1);
});
it("should reflect basic status moves", async () => {
game.override.moveset([ Moves.GROWL ]);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.move.select(Moves.GROWL);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(-1);
});
it("should individually bounce back multi-target moves when used by both targets in doubles", async () => {
game.override.battleType("double");
game.override.moveset([ Moves.GROWL, Moves.SPLASH ]);
await game.classicMode.startBattle([ Species.MAGIKARP, Species.MAGIKARP ]);
game.move.select(Moves.GROWL, 0);
game.move.select(Moves.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.battleType("double");
game.override.moveset([ Moves.GROWL, Moves.SPLASH ]);
game.override.enemyMoveset([ Moves.SPLASH, Moves.MAGIC_COAT ]);
await game.classicMode.startBattle([ Species.MAGIKARP, Species.MAGIKARP ]);
game.move.select(Moves.GROWL, 0);
game.move.select(Moves.SPLASH, 1);
await game.forceEnemyMove(Moves.SPLASH);
await game.forceEnemyMove(Moves.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([ Species.MAGIKARP ]);
game.scene.getEnemyPokemon()?.setStatStage(Stat.ATK, -6);
game.override.moveset([ Moves.GROWL ]);
game.move.select(Moves.GROWL);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(-1);
});
it("should not bounce back a move that was just bounced", async () => {
game.override.battleType("double");
game.override.ability(Abilities.MAGIC_BOUNCE);
game.override.moveset([ Moves.GROWL, Moves.MAGIC_COAT ]);
game.override.enemyMoveset([ Moves.SPLASH, Moves.MAGIC_COAT ]);
await game.classicMode.startBattle([ Species.MAGIKARP, Species.MAGIKARP ]);
game.move.select(Moves.MAGIC_COAT, 0);
game.move.select(Moves.GROWL, 1);
await game.forceEnemyMove(Moves.MAGIC_COAT);
await game.forceEnemyMove(Moves.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([ Species.MAGIKARP ]);
game.move.select(Moves.GROWL);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(-1);
});
it("should still bounce back a move from a mold breaker user", async () => {
game.override.ability(Abilities.MOLD_BREAKER);
game.override.moveset([ Moves.GROWL ]);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.move.select(Moves.GROWL);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(0);
expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(-1);
});
it("should only bounce spikes back once when both targets use magic coat in doubles", async () => {
game.override.battleType("double");
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.override.moveset([ Moves.SPIKES ]);
game.move.select(Moves.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.starterSpecies(Species.GASTLY);
await game.classicMode.startBattle([ Species.GASTLY ]);
game.override.moveset([ Moves.CURSE ]);
game.move.select(Moves.CURSE);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.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([ Moves.GROWL, Moves.ENCORE ]);
game.override.enemyMoveset([ Moves.MAGIC_COAT, Moves.TACKLE ]);
game.override.enemyAbility(Abilities.MAGIC_BOUNCE);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
// turn 1
game.move.select(Moves.GROWL);
await game.forceEnemyMove(Moves.MAGIC_COAT);
await game.toNextTurn();
// turn 2
game.move.select(Moves.ENCORE);
await game.forceEnemyMove(Moves.TACKLE);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon.getTag(BattlerTagType.ENCORE)!["moveId"]).toBe(Moves.TACKLE);
expect(enemyPokemon.getLastXMoves()[0].move).toBe(Moves.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.battleType("single");
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.override.moveset([ Moves.STOMPING_TANTRUM, Moves.CHARM ]);
const stomping_tantrum = allMoves[Moves.STOMPING_TANTRUM];
vi.spyOn(stomping_tantrum, "calculateBattlePower");
game.move.select(Moves.CHARM);
await game.toNextTurn();
game.move.select(Moves.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([ Moves.STOMPING_TANTRUM, Moves.SPLASH, Moves.CHARM ]);
await game.classicMode.startBattle([ Species.BULBASAUR ]);
const stomping_tantrum = allMoves[Moves.STOMPING_TANTRUM];
const enemy = game.scene.getEnemyPokemon()!;
vi.spyOn(stomping_tantrum, "calculateBattlePower");
game.move.select(Moves.SPORE);
await game.forceEnemyMove(Moves.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(Moves.GROWL);
await game.phaseInterceptor.to("BerryPhase");
expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(75);
});
it("should respect immunities when bouncing a move", async () => {
vi.spyOn(allMoves[Moves.THUNDER_WAVE], "accuracy", "get").mockReturnValue(100);
game.override.moveset([ Moves.THUNDER_WAVE, Moves.GROWL ]);
game.override.ability(Abilities.SOUNDPROOF);
await game.classicMode.startBattle([ Species.PHANPY ]);
// Turn 1 - thunder wave immunity test
game.move.select(Moves.THUNDER_WAVE);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()!.status).toBeUndefined();
// Turn 2 - soundproof immunity test
game.move.select(Moves.GROWL);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(0);
});
it("should bounce back a move before the accuracy check", async () => {
game.override.moveset([ Moves.SPORE ]);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const attacker = game.scene.getPlayerPokemon()!;
vi.spyOn(attacker, "getAccuracyMultiplier").mockReturnValue(0.0);
game.move.select(Moves.SPORE);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()!.status?.effect).toBe(StatusEffect.SLEEP);
});
it("should take the accuracy of the magic bounce user into account", async () => {
game.override.moveset([ Moves.SPORE ]);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const opponent = game.scene.getEnemyPokemon()!;
vi.spyOn(opponent, "getAccuracyMultiplier").mockReturnValue(0);
game.move.select(Moves.SPORE);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()!.status).toBeUndefined();
});
});