[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:
parent
702a6ba482
commit
5296966f70
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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);
|
||||||
|
|
277
src/data/move.ts
277
src/data/move.ts
|
@ -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)
|
||||||
|
|
|
@ -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",
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue