mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-01-30 20:57:13 +00:00
[Ability] Implement Parental Bond (#2384)
* Parental Bond basic implementation * Parental Bond unit tests * ESLint * PBond AbAttr rework, documentation, and multi-target unit tests * Update post-target move attribute logic For Parental Bond interaction. * AddSecondStrikeAbAttr now uses Constructor util * Unit tests for PBond + Multi-Lens interaction * Remove random damage spread in unit test * Add null checks to PBond AbAttr * Set player pokemon for unit test * Fixed Post Target check to account for KO's * Fix multi-strike moves applying effects at wrong times * Test cases for updated effect timing * Add Wake-Up Slap test case * Fix Fury Cutter/Echoed Voice multi-hit interaction * Fix Pay Day, Relic Song, and Fury Cutter (again) * Add early stopping to multi-hit moves * RecoilAttr now uses lastHitOnly * Add faint check to last hit logic
This commit is contained in:
parent
f7f8988cdb
commit
cd92d4d7d3
@ -9,7 +9,7 @@ import { Weather, WeatherType } from "./weather";
|
|||||||
import { BattlerTag, GroundedTag } from "./battler-tags";
|
import { BattlerTag, GroundedTag } from "./battler-tags";
|
||||||
import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect";
|
import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect";
|
||||||
import { Gender } from "./gender";
|
import { Gender } from "./gender";
|
||||||
import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr } from "./move";
|
import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, ChargeAttr, SacrificialAttr, SacrificialAttrOnHit } from "./move";
|
||||||
import { ArenaTagSide, ArenaTrapTag } from "./arena-tag";
|
import { ArenaTagSide, ArenaTrapTag } from "./arena-tag";
|
||||||
import { Stat, getStatName } from "./pokemon-stat";
|
import { Stat, getStatName } from "./pokemon-stat";
|
||||||
import { BerryModifier, PokemonHeldItemModifier } from "../modifier/modifier";
|
import { BerryModifier, PokemonHeldItemModifier } from "../modifier/modifier";
|
||||||
@ -1231,6 +1231,84 @@ export class PokemonTypeChangeAbAttr extends PreAttackAbAttr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for abilities that convert single-strike moves to two-strike moves (i.e. Parental Bond).
|
||||||
|
* @param damageMultiplier the damage multiplier for the second strike, relative to the first.
|
||||||
|
*/
|
||||||
|
export class AddSecondStrikeAbAttr extends PreAttackAbAttr {
|
||||||
|
private damageMultiplier: number;
|
||||||
|
|
||||||
|
constructor(damageMultiplier: number) {
|
||||||
|
super(false);
|
||||||
|
|
||||||
|
this.damageMultiplier = damageMultiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether this attribute can apply to a given move.
|
||||||
|
* @param {Move} move the move to which this attribute may apply
|
||||||
|
* @param numTargets the number of {@linkcode Pokemon} targeted by this move
|
||||||
|
* @returns true if the attribute can apply to the move, false otherwise
|
||||||
|
*/
|
||||||
|
canApplyPreAttack(move: Move, numTargets: integer): boolean {
|
||||||
|
/**
|
||||||
|
* Parental Bond cannot apply to multi-hit moves, charging moves, or
|
||||||
|
* moves that cause the user to faint.
|
||||||
|
*/
|
||||||
|
const exceptAttrs: Constructor<MoveAttr>[] = [
|
||||||
|
MultiHitAttr,
|
||||||
|
ChargeAttr,
|
||||||
|
SacrificialAttr,
|
||||||
|
SacrificialAttrOnHit
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Parental Bond cannot apply to these specific moves */
|
||||||
|
const exceptMoves: Moves[] = [
|
||||||
|
Moves.FLING,
|
||||||
|
Moves.UPROAR,
|
||||||
|
Moves.ROLLOUT,
|
||||||
|
Moves.ICE_BALL,
|
||||||
|
Moves.ENDEAVOR
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Also check if this move is an Attack move and if it's only targeting one Pokemon */
|
||||||
|
return numTargets === 1
|
||||||
|
&& !exceptAttrs.some(attr => move.hasAttr(attr))
|
||||||
|
&& !exceptMoves.some(id => move.id === id)
|
||||||
|
&& move.category !== MoveCategory.STATUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If conditions are met, this doubles the move's hit count (via args[1])
|
||||||
|
* or multiplies the damage of secondary strikes (via args[2])
|
||||||
|
* @param {Pokemon} pokemon the Pokemon using the move
|
||||||
|
* @param passive n/a
|
||||||
|
* @param defender n/a
|
||||||
|
* @param {Move} move the move used by the ability source
|
||||||
|
* @param args\[0\] the number of Pokemon this move is targeting
|
||||||
|
* @param {Utils.IntegerHolder} args\[1\] the number of strikes with this move
|
||||||
|
* @param {Utils.NumberHolder} args\[2\] the damage multiplier for the current strike
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
applyPreAttack(pokemon: Pokemon, passive: boolean, defender: Pokemon, move: Move, args: any[]): boolean {
|
||||||
|
const numTargets = args[0] as integer;
|
||||||
|
const hitCount = args[1] as Utils.IntegerHolder;
|
||||||
|
const multiplier = args[2] as Utils.NumberHolder;
|
||||||
|
|
||||||
|
if (this.canApplyPreAttack(move, numTargets)) {
|
||||||
|
if (!!hitCount?.value) {
|
||||||
|
hitCount.value *= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!!multiplier?.value && pokemon.turnData.hitsLeft % 2 === 1) {
|
||||||
|
multiplier.value *= this.damageMultiplier;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for abilities that boost the damage of moves
|
* Class for abilities that boost the damage of moves
|
||||||
* For abilities that boost the base power of moves, see VariableMovePowerAbAttr
|
* For abilities that boost the base power of moves, see VariableMovePowerAbAttr
|
||||||
@ -4632,7 +4710,7 @@ export function initAbilities() {
|
|||||||
new Ability(Abilities.AERILATE, 6)
|
new Ability(Abilities.AERILATE, 6)
|
||||||
.attr(MoveTypeChangeAttr, Type.FLYING, 1.2, (user, target, move) => move.type === Type.NORMAL),
|
.attr(MoveTypeChangeAttr, Type.FLYING, 1.2, (user, target, move) => move.type === Type.NORMAL),
|
||||||
new Ability(Abilities.PARENTAL_BOND, 6)
|
new Ability(Abilities.PARENTAL_BOND, 6)
|
||||||
.unimplemented(),
|
.attr(AddSecondStrikeAbAttr, 0.25),
|
||||||
new Ability(Abilities.DARK_AURA, 6)
|
new Ability(Abilities.DARK_AURA, 6)
|
||||||
.attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => getPokemonMessage(pokemon, " is radiating a Dark Aura!"))
|
.attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => getPokemonMessage(pokemon, " is radiating a Dark Aura!"))
|
||||||
.attr(FieldMoveTypePowerBoostAbAttr, Type.DARK, 4 / 3),
|
.attr(FieldMoveTypePowerBoostAbAttr, Type.DARK, 4 / 3),
|
||||||
|
@ -896,6 +896,13 @@ export class ProtectedTag extends BattlerTag {
|
|||||||
if (lapseType === BattlerTagLapseType.CUSTOM) {
|
if (lapseType === BattlerTagLapseType.CUSTOM) {
|
||||||
new CommonBattleAnim(CommonAnim.PROTECT, pokemon).play(pokemon.scene);
|
new CommonBattleAnim(CommonAnim.PROTECT, pokemon).play(pokemon.scene);
|
||||||
pokemon.scene.queueMessage(i18next.t("battle:battlerTagsProtectedLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
|
pokemon.scene.queueMessage(i18next.t("battle:battlerTagsProtectedLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
|
||||||
|
|
||||||
|
// Stop multi-hit moves early
|
||||||
|
const effectPhase = pokemon.scene.getCurrentPhase();
|
||||||
|
if (effectPhase instanceof MoveEffectPhase) {
|
||||||
|
const attacker = effectPhase.getPokemon();
|
||||||
|
attacker.stopMultiHit();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -811,11 +811,14 @@ export class MoveEffectAttr extends MoveAttr {
|
|||||||
public trigger: MoveEffectTrigger;
|
public trigger: MoveEffectTrigger;
|
||||||
/** Should this effect only apply on the first hit? */
|
/** Should this effect only apply on the first hit? */
|
||||||
public firstHitOnly: boolean;
|
public firstHitOnly: boolean;
|
||||||
|
/** Should this effect only apply on the last hit? */
|
||||||
|
public lastHitOnly: boolean;
|
||||||
|
|
||||||
constructor(selfTarget?: boolean, trigger?: MoveEffectTrigger, firstHitOnly: boolean = false) {
|
constructor(selfTarget?: boolean, trigger?: MoveEffectTrigger, firstHitOnly: boolean = false, lastHitOnly: boolean = false) {
|
||||||
super(selfTarget);
|
super(selfTarget);
|
||||||
this.trigger = trigger !== undefined ? trigger : MoveEffectTrigger.POST_APPLY;
|
this.trigger = trigger !== undefined ? trigger : MoveEffectTrigger.POST_APPLY;
|
||||||
this.firstHitOnly = firstHitOnly;
|
this.firstHitOnly = firstHitOnly;
|
||||||
|
this.lastHitOnly = lastHitOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1064,7 +1067,7 @@ export class RecoilAttr extends MoveEffectAttr {
|
|||||||
private unblockable: boolean;
|
private unblockable: boolean;
|
||||||
|
|
||||||
constructor(useHp: boolean = false, damageRatio: number = 0.25, unblockable: boolean = false) {
|
constructor(useHp: boolean = false, damageRatio: number = 0.25, unblockable: boolean = false) {
|
||||||
super(true);
|
super(true, MoveEffectTrigger.POST_APPLY, false, true);
|
||||||
|
|
||||||
this.useHp = useHp;
|
this.useHp = useHp;
|
||||||
this.damageRatio = damageRatio;
|
this.damageRatio = damageRatio;
|
||||||
@ -1085,8 +1088,8 @@ export class RecoilAttr extends MoveEffectAttr {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const recoilDamage = Math.max(Math.floor((!this.useHp ? user.turnData.currDamageDealt : user.getMaxHp()) * this.damageRatio),
|
const recoilDamage = Math.max(Math.floor((!this.useHp ? user.turnData.damageDealt : user.getMaxHp()) * this.damageRatio),
|
||||||
user.turnData.currDamageDealt ? 1 : 0);
|
user.turnData.damageDealt ? 1 : 0);
|
||||||
if (!recoilDamage) {
|
if (!recoilDamage) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -2013,7 +2016,7 @@ export class HealStatusEffectAttr extends MoveEffectAttr {
|
|||||||
* @param ...effects - List of status effects to cure
|
* @param ...effects - List of status effects to cure
|
||||||
*/
|
*/
|
||||||
constructor(selfTarget: boolean, ...effects: StatusEffect[]) {
|
constructor(selfTarget: boolean, ...effects: StatusEffect[]) {
|
||||||
super(selfTarget);
|
super(selfTarget, MoveEffectTrigger.POST_APPLY, false, true);
|
||||||
|
|
||||||
this.effects = effects;
|
this.effects = effects;
|
||||||
}
|
}
|
||||||
@ -2770,7 +2773,7 @@ export class DoublePowerChanceAttr extends VariablePowerAttr {
|
|||||||
export abstract class ConsecutiveUsePowerMultiplierAttr extends MovePowerMultiplierAttr {
|
export abstract class ConsecutiveUsePowerMultiplierAttr extends MovePowerMultiplierAttr {
|
||||||
constructor(limit: integer, resetOnFail: boolean, resetOnLimit?: boolean, ...comboMoves: Moves[]) {
|
constructor(limit: integer, resetOnFail: boolean, resetOnLimit?: boolean, ...comboMoves: Moves[]) {
|
||||||
super((user: Pokemon, target: Pokemon, move: Move): number => {
|
super((user: Pokemon, target: Pokemon, move: Move): number => {
|
||||||
const moveHistory = user.getMoveHistory().reverse().slice(1);
|
const moveHistory = user.getLastXMoves(limit + 1).slice(1);
|
||||||
|
|
||||||
let count = 0;
|
let count = 0;
|
||||||
let turnMove: TurnMove;
|
let turnMove: TurnMove;
|
||||||
@ -3139,8 +3142,13 @@ export class PunishmentPowerAttr extends VariablePowerAttr {
|
|||||||
|
|
||||||
export class PresentPowerAttr extends VariablePowerAttr {
|
export class PresentPowerAttr extends VariablePowerAttr {
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
|
/**
|
||||||
|
* If this move is multi-hit, and this attribute is applied to any hit
|
||||||
|
* other than the first, this move cannot result in a heal.
|
||||||
|
*/
|
||||||
|
const firstHit = (user.turnData.hitCount === user.turnData.hitsLeft);
|
||||||
|
|
||||||
const powerSeed = Utils.randSeedInt(100);
|
const powerSeed = Utils.randSeedInt(firstHit ? 100 : 80);
|
||||||
if (powerSeed <= 40) {
|
if (powerSeed <= 40) {
|
||||||
(args[0] as Utils.NumberHolder).value = 40;
|
(args[0] as Utils.NumberHolder).value = 40;
|
||||||
} else if (40 < powerSeed && powerSeed <= 70) {
|
} else if (40 < powerSeed && powerSeed <= 70) {
|
||||||
@ -3148,6 +3156,8 @@ export class PresentPowerAttr extends VariablePowerAttr {
|
|||||||
} else if (70 < powerSeed && powerSeed <= 80) {
|
} else if (70 < powerSeed && powerSeed <= 80) {
|
||||||
(args[0] as Utils.NumberHolder).value = 120;
|
(args[0] as Utils.NumberHolder).value = 120;
|
||||||
} else if (80 < powerSeed && powerSeed <= 100) {
|
} else if (80 < powerSeed && powerSeed <= 100) {
|
||||||
|
// If this move is multi-hit, disable all other hits
|
||||||
|
user.stopMultiHit();
|
||||||
target.scene.unshiftPhase(new PokemonHealPhase(target.scene, target.getBattlerIndex(),
|
target.scene.unshiftPhase(new PokemonHealPhase(target.scene, target.getBattlerIndex(),
|
||||||
Math.max(Math.floor(target.getMaxHp() / 4), 1), getPokemonMessage(target, " regained\nhealth!"), true));
|
Math.max(Math.floor(target.getMaxHp() / 4), 1), getPokemonMessage(target, " regained\nhealth!"), true));
|
||||||
}
|
}
|
||||||
@ -3905,8 +3915,8 @@ export class AddBattlerTagAttr extends MoveEffectAttr {
|
|||||||
public turnCountMax: integer;
|
public turnCountMax: integer;
|
||||||
private failOnOverlap: boolean;
|
private failOnOverlap: boolean;
|
||||||
|
|
||||||
constructor(tagType: BattlerTagType, selfTarget: boolean = false, failOnOverlap: boolean = false, turnCountMin: integer = 0, turnCountMax?: integer) {
|
constructor(tagType: BattlerTagType, selfTarget: boolean = false, failOnOverlap: boolean = false, turnCountMin: integer = 0, turnCountMax?: integer, lastHitOnly: boolean = false) {
|
||||||
super(selfTarget);
|
super(selfTarget, MoveEffectTrigger.POST_APPLY, false, lastHitOnly);
|
||||||
|
|
||||||
this.tagType = tagType;
|
this.tagType = tagType;
|
||||||
this.turnCountMin = turnCountMin;
|
this.turnCountMin = turnCountMin;
|
||||||
@ -4071,7 +4081,7 @@ export class ConfuseAttr extends AddBattlerTagAttr {
|
|||||||
|
|
||||||
export class RechargeAttr extends AddBattlerTagAttr {
|
export class RechargeAttr extends AddBattlerTagAttr {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(BattlerTagType.RECHARGING, true);
|
super(BattlerTagType.RECHARGING, true, false, 1, 1, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4468,7 +4478,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
|||||||
private batonPass: boolean;
|
private batonPass: boolean;
|
||||||
|
|
||||||
constructor(user?: boolean, batonPass?: boolean) {
|
constructor(user?: boolean, batonPass?: boolean) {
|
||||||
super(false, MoveEffectTrigger.POST_APPLY, true);
|
super(false, MoveEffectTrigger.POST_APPLY, false, true);
|
||||||
this.user = !!user;
|
this.user = !!user;
|
||||||
this.batonPass = !!batonPass;
|
this.batonPass = !!batonPass;
|
||||||
}
|
}
|
||||||
@ -4583,7 +4593,7 @@ export class RemoveTypeAttr extends MoveEffectAttr {
|
|||||||
private messageCallback: ((user: Pokemon) => void) | undefined;
|
private messageCallback: ((user: Pokemon) => void) | undefined;
|
||||||
|
|
||||||
constructor(removedType: Type, messageCallback?: (user: Pokemon) => void) {
|
constructor(removedType: Type, messageCallback?: (user: Pokemon) => void) {
|
||||||
super(true, MoveEffectTrigger.POST_APPLY);
|
super(true, MoveEffectTrigger.POST_TARGET);
|
||||||
this.removedType = removedType;
|
this.removedType = removedType;
|
||||||
this.messageCallback = messageCallback;
|
this.messageCallback = messageCallback;
|
||||||
|
|
||||||
@ -5391,7 +5401,7 @@ export class DiscourageFrequentUseAttr extends MoveAttr {
|
|||||||
|
|
||||||
export class MoneyAttr extends MoveEffectAttr {
|
export class MoneyAttr extends MoveEffectAttr {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(true, MoveEffectTrigger.HIT);
|
super(true, MoveEffectTrigger.HIT, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move): boolean {
|
||||||
@ -6947,7 +6957,7 @@ export function initMoves() {
|
|||||||
.target(MoveTarget.BOTH_SIDES)
|
.target(MoveTarget.BOTH_SIDES)
|
||||||
.unimplemented(),
|
.unimplemented(),
|
||||||
new AttackMove(Moves.SMACK_DOWN, Type.ROCK, MoveCategory.PHYSICAL, 50, 100, 15, 100, 0, 5)
|
new AttackMove(Moves.SMACK_DOWN, Type.ROCK, MoveCategory.PHYSICAL, 50, 100, 15, 100, 0, 5)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false)
|
.attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false, 1, 1, true)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED)
|
.attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED)
|
||||||
.attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN])
|
.attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN])
|
||||||
.attr(HitsTagAttr, BattlerTagType.FLYING, false)
|
.attr(HitsTagAttr, BattlerTagType.FLYING, false)
|
||||||
@ -7331,14 +7341,14 @@ export function initMoves() {
|
|||||||
.triageMove(),
|
.triageMove(),
|
||||||
new AttackMove(Moves.THOUSAND_ARROWS, Type.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
|
new AttackMove(Moves.THOUSAND_ARROWS, Type.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
|
||||||
.attr(NeutralDamageAgainstFlyingTypeMultiplierAttr)
|
.attr(NeutralDamageAgainstFlyingTypeMultiplierAttr)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false)
|
.attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false, 1, 1, true)
|
||||||
.attr(HitsTagAttr, BattlerTagType.FLYING, false)
|
.attr(HitsTagAttr, BattlerTagType.FLYING, false)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED)
|
.attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED)
|
||||||
.attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN])
|
.attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN])
|
||||||
.makesContact(false)
|
.makesContact(false)
|
||||||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||||
new AttackMove(Moves.THOUSAND_WAVES, Type.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
|
new AttackMove(Moves.THOUSAND_WAVES, Type.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1)
|
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1, 1, true)
|
||||||
.makesContact(false)
|
.makesContact(false)
|
||||||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||||
new AttackMove(Moves.LANDS_WRATH, Type.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
|
new AttackMove(Moves.LANDS_WRATH, Type.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
|
||||||
@ -7480,7 +7490,7 @@ export function initMoves() {
|
|||||||
new SelfStatusMove(Moves.BANEFUL_BUNKER, Type.POISON, -1, 10, -1, 4, 7)
|
new SelfStatusMove(Moves.BANEFUL_BUNKER, Type.POISON, -1, 10, -1, 4, 7)
|
||||||
.attr(ProtectAttr, BattlerTagType.BANEFUL_BUNKER),
|
.attr(ProtectAttr, BattlerTagType.BANEFUL_BUNKER),
|
||||||
new AttackMove(Moves.SPIRIT_SHACKLE, Type.GHOST, MoveCategory.PHYSICAL, 80, 100, 10, 100, 0, 7)
|
new AttackMove(Moves.SPIRIT_SHACKLE, Type.GHOST, MoveCategory.PHYSICAL, 80, 100, 10, 100, 0, 7)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1)
|
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1, 1, true)
|
||||||
.makesContact(false),
|
.makesContact(false),
|
||||||
new AttackMove(Moves.DARKEST_LARIAT, Type.DARK, MoveCategory.PHYSICAL, 85, 100, 10, -1, 0, 7)
|
new AttackMove(Moves.DARKEST_LARIAT, Type.DARK, MoveCategory.PHYSICAL, 85, 100, 10, -1, 0, 7)
|
||||||
.attr(IgnoreOpponentStatChangesAttr),
|
.attr(IgnoreOpponentStatChangesAttr),
|
||||||
@ -7524,7 +7534,7 @@ export function initMoves() {
|
|||||||
.attr(HealOnAllyAttr, 0.5, true, false)
|
.attr(HealOnAllyAttr, 0.5, true, false)
|
||||||
.ballBombMove(),
|
.ballBombMove(),
|
||||||
new AttackMove(Moves.ANCHOR_SHOT, Type.STEEL, MoveCategory.PHYSICAL, 80, 100, 20, 100, 0, 7)
|
new AttackMove(Moves.ANCHOR_SHOT, Type.STEEL, MoveCategory.PHYSICAL, 80, 100, 20, 100, 0, 7)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1),
|
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1, 1, true),
|
||||||
new StatusMove(Moves.PSYCHIC_TERRAIN, Type.PSYCHIC, -1, 10, -1, 0, 7)
|
new StatusMove(Moves.PSYCHIC_TERRAIN, Type.PSYCHIC, -1, 10, -1, 0, 7)
|
||||||
.attr(TerrainChangeAttr, TerrainType.PSYCHIC)
|
.attr(TerrainChangeAttr, TerrainType.PSYCHIC)
|
||||||
.target(MoveTarget.BOTH_SIDES),
|
.target(MoveTarget.BOTH_SIDES),
|
||||||
|
@ -23,7 +23,7 @@ import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HelpingHandTag
|
|||||||
import { WeatherType } from "../data/weather";
|
import { WeatherType } from "../data/weather";
|
||||||
import { TempBattleStat } from "../data/temp-battle-stat";
|
import { TempBattleStat } from "../data/temp-battle-stat";
|
||||||
import { ArenaTagSide, WeakenMoveScreenTag, WeakenMoveTypeTag } from "../data/arena-tag";
|
import { ArenaTagSide, WeakenMoveScreenTag, WeakenMoveTypeTag } from "../data/arena-tag";
|
||||||
import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, MoveTypeChangeAttr, PreApplyBattlerTagAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, VariableMovePowerAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AllyMoveCategoryPowerBoostAbAttr, FieldMoveTypePowerBoostAbAttr } from "../data/ability";
|
import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, MoveTypeChangeAttr, PreApplyBattlerTagAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, VariableMovePowerAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AllyMoveCategoryPowerBoostAbAttr, FieldMoveTypePowerBoostAbAttr, AddSecondStrikeAbAttr } from "../data/ability";
|
||||||
import PokemonData from "../system/pokemon-data";
|
import PokemonData from "../system/pokemon-data";
|
||||||
import { BattlerIndex } from "../battle";
|
import { BattlerIndex } from "../battle";
|
||||||
import { Mode } from "../ui/ui";
|
import { Mode } from "../ui/ui";
|
||||||
@ -1800,6 +1800,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (cancelled.value) {
|
if (cancelled.value) {
|
||||||
|
source.stopMultiHit();
|
||||||
result = HitResult.NO_EFFECT;
|
result = HitResult.NO_EFFECT;
|
||||||
} else {
|
} else {
|
||||||
const typeBoost = source.findTag(t => t instanceof TypeBoostTag && t.boostedType === move.type) as TypeBoostTag;
|
const typeBoost = source.findTag(t => t instanceof TypeBoostTag && t.boostedType === move.type) as TypeBoostTag;
|
||||||
@ -1885,8 +1886,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
applyMoveAttrs(VariableAtkAttr, source, this, move, sourceAtk);
|
applyMoveAttrs(VariableAtkAttr, source, this, move, sourceAtk);
|
||||||
applyMoveAttrs(VariableDefAttr, source, this, move, targetDef);
|
applyMoveAttrs(VariableDefAttr, source, this, move, targetDef);
|
||||||
|
|
||||||
|
const twoStrikeMultiplier = new Utils.NumberHolder(1);
|
||||||
|
applyPreAttackAbAttrs(AddSecondStrikeAbAttr, source, this, move, 1, new Utils.IntegerHolder(0), twoStrikeMultiplier);
|
||||||
|
|
||||||
if (!isTypeImmune) {
|
if (!isTypeImmune) {
|
||||||
damage.value = Math.ceil(((((2 * source.level / 5 + 2) * power.value * sourceAtk.value / targetDef.value) / 50) + 2) * stabMultiplier.value * typeMultiplier.value * arenaAttackTypeMultiplier.value * screenMultiplier.value * ((this.scene.randBattleSeedInt(15) + 85) / 100) * criticalMultiplier.value);
|
damage.value = Math.ceil(((((2 * source.level / 5 + 2) * power.value * sourceAtk.value / targetDef.value) / 50) + 2) * stabMultiplier.value * typeMultiplier.value * arenaAttackTypeMultiplier.value * screenMultiplier.value * twoStrikeMultiplier.value * ((this.scene.randBattleSeedInt(15) + 85) / 100) * criticalMultiplier.value);
|
||||||
if (isPhysical && source.status && source.status.effect === StatusEffect.BURN) {
|
if (isPhysical && source.status && source.status.effect === StatusEffect.BURN) {
|
||||||
if (!move.hasAttr(BypassBurnDamageReductionAttr)) {
|
if (!move.hasAttr(BypassBurnDamageReductionAttr)) {
|
||||||
const burnDamageReductionCancelled = new Utils.BooleanHolder(false);
|
const burnDamageReductionCancelled = new Utils.BooleanHolder(false);
|
||||||
@ -2251,6 +2255,18 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
return this.summonData.moveQueue;
|
return this.summonData.moveQueue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this Pokemon is using a multi-hit move, stop the move
|
||||||
|
* after the next hit resolves.
|
||||||
|
*/
|
||||||
|
stopMultiHit(): void {
|
||||||
|
if (!this.turnData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.turnData.hitCount = 1;
|
||||||
|
this.turnData.hitsLeft = 1;
|
||||||
|
}
|
||||||
|
|
||||||
changeForm(formChange: SpeciesFormChange): Promise<void> {
|
changeForm(formChange: SpeciesFormChange): Promise<void> {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
this.formIndex = Math.max(this.species.forms.findIndex(f => f.formKey === formChange.formKey), 0);
|
this.formIndex = Math.max(this.species.forms.findIndex(f => f.formKey === formChange.formKey), 0);
|
||||||
|
@ -26,7 +26,7 @@ import { Gender } from "./data/gender";
|
|||||||
import { Weather, WeatherType, getRandomWeatherType, getTerrainBlockMessage, getWeatherDamageMessage, getWeatherLapseMessage } from "./data/weather";
|
import { Weather, WeatherType, getRandomWeatherType, getTerrainBlockMessage, getWeatherDamageMessage, getWeatherLapseMessage } from "./data/weather";
|
||||||
import { TempBattleStat } from "./data/temp-battle-stat";
|
import { TempBattleStat } from "./data/temp-battle-stat";
|
||||||
import { ArenaTagSide, ArenaTrapTag, MistTag, TrickRoomTag } from "./data/arena-tag";
|
import { ArenaTagSide, ArenaTrapTag, MistTag, TrickRoomTag } from "./data/arena-tag";
|
||||||
import { CheckTrappedAbAttr, IgnoreOpponentStatChangesAbAttr, IgnoreOpponentEvasionAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, BlockRedirectAbAttr, RunSuccessAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, BattleStatMultiplierAbAttr, applyBattleStatMultiplierAbAttrs, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, PokemonTypeChangeAbAttr, applyPreAttackAbAttrs, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr, MaxMultiHitAbAttr, HealFromBerryUseAbAttr, WonderSkinAbAttr, applyPreDefendAbAttrs, IgnoreMoveEffectsAbAttr, BlockStatusDamageAbAttr, BypassSpeedChanceAbAttr } from "./data/ability";
|
import { CheckTrappedAbAttr, IgnoreOpponentStatChangesAbAttr, IgnoreOpponentEvasionAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, BlockRedirectAbAttr, RunSuccessAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, BattleStatMultiplierAbAttr, applyBattleStatMultiplierAbAttrs, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, PokemonTypeChangeAbAttr, applyPreAttackAbAttrs, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr, MaxMultiHitAbAttr, HealFromBerryUseAbAttr, WonderSkinAbAttr, applyPreDefendAbAttrs, IgnoreMoveEffectsAbAttr, BlockStatusDamageAbAttr, BypassSpeedChanceAbAttr, AddSecondStrikeAbAttr } from "./data/ability";
|
||||||
import { Unlockables, getUnlockableName } from "./system/unlockables";
|
import { Unlockables, getUnlockableName } from "./system/unlockables";
|
||||||
import { getBiomeKey } from "./field/arena";
|
import { getBiomeKey } from "./field/arena";
|
||||||
import { BattleType, BattlerIndex, TurnCommand } from "./battle";
|
import { BattleType, BattlerIndex, TurnCommand } from "./battle";
|
||||||
@ -2878,6 +2878,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
const hitCount = new Utils.IntegerHolder(1);
|
const hitCount = new Utils.IntegerHolder(1);
|
||||||
// Assume single target for multi hit
|
// Assume single target for multi hit
|
||||||
applyMoveAttrs(MultiHitAttr, user, this.getTarget(), move, hitCount);
|
applyMoveAttrs(MultiHitAttr, user, this.getTarget(), move, hitCount);
|
||||||
|
applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, targets.length, hitCount, new Utils.IntegerHolder(0));
|
||||||
if (move instanceof AttackMove && !move.hasAttr(FixedDamageAttr)) {
|
if (move instanceof AttackMove && !move.hasAttr(FixedDamageAttr)) {
|
||||||
this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, hitCount, new Utils.IntegerHolder(0));
|
this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, hitCount, new Utils.IntegerHolder(0));
|
||||||
}
|
}
|
||||||
@ -2885,7 +2886,6 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const moveHistoryEntry = { move: this.move.moveId, targets: this.targets, result: MoveResult.PENDING, virtual: this.move.virtual };
|
const moveHistoryEntry = { move: this.move.moveId, targets: this.targets, result: MoveResult.PENDING, virtual: this.move.virtual };
|
||||||
user.pushMoveHistory(moveHistoryEntry);
|
|
||||||
|
|
||||||
const targetHitChecks = Object.fromEntries(targets.map(p => [p.getBattlerIndex(), this.hitCheck(p)]));
|
const targetHitChecks = Object.fromEntries(targets.map(p => [p.getBattlerIndex(), this.hitCheck(p)]));
|
||||||
const activeTargets = targets.map(t => t.isActive(true));
|
const activeTargets = targets.map(t => t.isActive(true));
|
||||||
@ -2900,6 +2900,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
this.scene.queueMessage(i18next.t("battle:attackFailed"));
|
this.scene.queueMessage(i18next.t("battle:attackFailed"));
|
||||||
moveHistoryEntry.result = MoveResult.FAIL;
|
moveHistoryEntry.result = MoveResult.FAIL;
|
||||||
}
|
}
|
||||||
|
user.pushMoveHistory(moveHistoryEntry);
|
||||||
return this.end();
|
return this.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2921,25 +2922,33 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
|
|
||||||
const isProtected = !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target) && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType));
|
const isProtected = !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target) && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType));
|
||||||
|
|
||||||
const firstHit = moveHistoryEntry.result !== MoveResult.SUCCESS;
|
const firstHit = (user.turnData.hitsLeft === user.turnData.hitCount);
|
||||||
|
|
||||||
|
if (firstHit) {
|
||||||
|
user.pushMoveHistory(moveHistoryEntry);
|
||||||
|
}
|
||||||
|
|
||||||
moveHistoryEntry.result = MoveResult.SUCCESS;
|
moveHistoryEntry.result = MoveResult.SUCCESS;
|
||||||
|
|
||||||
const hitResult = !isProtected ? target.apply(user, move) : HitResult.NO_EFFECT;
|
const hitResult = !isProtected ? target.apply(user, move) : HitResult.NO_EFFECT;
|
||||||
|
|
||||||
this.scene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger);
|
const lastHit = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive());
|
||||||
|
|
||||||
|
if (lastHit) {
|
||||||
|
this.scene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger);
|
||||||
|
}
|
||||||
|
|
||||||
applyAttrs.push(new Promise(resolve => {
|
applyAttrs.push(new Promise(resolve => {
|
||||||
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.PRE_APPLY && (!attr.firstHitOnly || firstHit),
|
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.PRE_APPLY && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit),
|
||||||
user, target, move).then(() => {
|
user, target, move).then(() => {
|
||||||
if (hitResult !== HitResult.FAIL) {
|
if (hitResult !== HitResult.FAIL) {
|
||||||
const chargeEffect = !!move.getAttrs(ChargeAttr).find(ca => ca.usedChargeEffect(user, this.getTarget(), move));
|
const chargeEffect = !!move.getAttrs(ChargeAttr).find(ca => ca.usedChargeEffect(user, this.getTarget(), move));
|
||||||
// Charge attribute with charge effect takes all effect attributes and applies them to charge stage, so ignore them if this is present
|
// Charge attribute with charge effect takes all effect attributes and applies them to charge stage, so ignore them if this is present
|
||||||
Utils.executeIf(!chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_APPLY
|
Utils.executeIf(!chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_APPLY
|
||||||
&& attr.selfTarget && (!attr.firstHitOnly || firstHit), user, target, move)).then(() => {
|
&& attr.selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, move)).then(() => {
|
||||||
if (hitResult !== HitResult.NO_EFFECT) {
|
if (hitResult !== HitResult.NO_EFFECT) {
|
||||||
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.POST_APPLY
|
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.POST_APPLY
|
||||||
&& !(attr as MoveEffectAttr).selfTarget && (!attr.firstHitOnly || firstHit), user, target, this.move.getMove()).then(() => {
|
&& !(attr as MoveEffectAttr).selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, this.move.getMove()).then(() => {
|
||||||
if (hitResult < HitResult.NO_EFFECT && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr)) {
|
if (hitResult < HitResult.NO_EFFECT && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr)) {
|
||||||
const flinched = new Utils.BooleanHolder(false);
|
const flinched = new Utils.BooleanHolder(false);
|
||||||
user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched);
|
user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched);
|
||||||
@ -2947,7 +2956,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id);
|
target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Utils.executeIf(!isProtected && !chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.HIT && (!attr.firstHitOnly || firstHit),
|
Utils.executeIf(!isProtected && !chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.HIT && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit),
|
||||||
user, target, this.move.getMove()).then(() => {
|
user, target, this.move.getMove()).then(() => {
|
||||||
return Utils.executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult).then(() => {
|
return Utils.executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult).then(() => {
|
||||||
if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) {
|
if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) {
|
||||||
@ -2974,14 +2983,17 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
// Trigger effect which should only apply one time after all targeted effects have already applied
|
// Trigger effect which should only apply one time on the last hit after all targeted effects have already applied
|
||||||
const postTarget = applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET,
|
const postTarget = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive()) ?
|
||||||
user, null, move);
|
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET, user, null, move) :
|
||||||
|
null;
|
||||||
|
|
||||||
if (applyAttrs.length) { // If there is a pending asynchronous move effect, do this after
|
if (!!postTarget) {
|
||||||
applyAttrs[applyAttrs.length - 1]?.then(() => postTarget);
|
if (applyAttrs.length) { // If there is a pending asynchronous move effect, do this after
|
||||||
} else { // Otherwise, push a new asynchronous move effect
|
applyAttrs[applyAttrs.length - 1]?.then(() => postTarget);
|
||||||
applyAttrs.push(postTarget);
|
} else { // Otherwise, push a new asynchronous move effect
|
||||||
|
applyAttrs.push(postTarget);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Promise.allSettled(applyAttrs).then(() => this.end());
|
Promise.allSettled(applyAttrs).then(() => this.end());
|
||||||
|
612
src/test/abilities/parental_bond.test.ts
Normal file
612
src/test/abilities/parental_bond.test.ts
Normal file
@ -0,0 +1,612 @@
|
|||||||
|
import Phaser from "phaser";
|
||||||
|
import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
|
import GameManager from "../utils/gameManager";
|
||||||
|
import * as Overrides from "#app/overrides";
|
||||||
|
import { Species } from "#enums/species";
|
||||||
|
import { Abilities } from "#enums/abilities";
|
||||||
|
import { Moves } from "#enums/moves";
|
||||||
|
import { getMovePosition } from "../utils/gameManagerUtils";
|
||||||
|
import { CommandPhase, DamagePhase, MoveEffectPhase, MoveEndPhase, TurnEndPhase } from "#app/phases.js";
|
||||||
|
import { BattleStat } from "#app/data/battle-stat.js";
|
||||||
|
import { Type } from "#app/data/type.js";
|
||||||
|
import { BattlerTagType } from "#app/enums/battler-tag-type.js";
|
||||||
|
import { StatusEffect } from "#app/data/status-effect.js";
|
||||||
|
|
||||||
|
const TIMEOUT = 20 * 1000;
|
||||||
|
|
||||||
|
describe("Abilities - Parental Bond", () => {
|
||||||
|
let phaserGame: Phaser.Game;
|
||||||
|
let game: GameManager;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
vi.spyOn(Overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
|
||||||
|
vi.spyOn(Overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.PARENTAL_BOND);
|
||||||
|
vi.spyOn(Overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.SNORLAX);
|
||||||
|
vi.spyOn(Overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.INSOMNIA);
|
||||||
|
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]);
|
||||||
|
vi.spyOn(Overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(100);
|
||||||
|
vi.spyOn(Overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability should add second strike to attack move",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.CHARIZARD]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||||
|
expect(enemyPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
let enemyStartingHp = enemyPokemon.hp;
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.TACKLE));
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||||
|
vi.spyOn(game.scene, "randBattleSeedInt").mockReturnValue(15);
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(DamagePhase);
|
||||||
|
const firstStrikeDamage = enemyStartingHp - enemyPokemon.hp;
|
||||||
|
enemyStartingHp = enemyPokemon.hp;
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase, false);
|
||||||
|
|
||||||
|
const secondStrikeDamage = enemyStartingHp - enemyPokemon.hp;
|
||||||
|
|
||||||
|
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||||
|
expect(secondStrikeDamage).toBe(Math.ceil(0.25 * firstStrikeDamage));
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability should apply secondary effects to both strikes",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.POWER_UP_PUNCH]);
|
||||||
|
vi.spyOn(Overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.AMOONGUSS);
|
||||||
|
|
||||||
|
await game.startBattle([Species.CHARIZARD]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||||
|
expect(enemyPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.POWER_UP_PUNCH));
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase, false);
|
||||||
|
|
||||||
|
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||||
|
expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(2);
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability should not apply to Status moves",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.BABY_DOLL_EYES]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.CHARIZARD]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||||
|
expect(enemyPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.BABY_DOLL_EYES));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase, false);
|
||||||
|
|
||||||
|
expect(enemyPokemon.summonData.battleStats[BattleStat.ATK]).toBe(-1);
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability should not apply to multi-hit moves",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.DOUBLE_HIT]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.CHARIZARD]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||||
|
expect(enemyPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.DOUBLE_HIT));
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||||
|
|
||||||
|
vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true);
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase, false);
|
||||||
|
|
||||||
|
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability should not apply to self-sacrifice moves",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SELF_DESTRUCT]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.CHARIZARD]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||||
|
expect(enemyPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.SELF_DESTRUCT));
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(DamagePhase, false);
|
||||||
|
|
||||||
|
expect(leadPokemon.turnData.hitCount).toBe(1);
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability should not apply to Rollout",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.ROLLOUT]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.CHARIZARD]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||||
|
expect(enemyPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.ROLLOUT));
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||||
|
vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true);
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(DamagePhase, false);
|
||||||
|
|
||||||
|
expect(leadPokemon.turnData.hitCount).toBe(1);
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability should not apply multiplier to fixed-damage moves",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.DRAGON_RAGE]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.CHARIZARD]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||||
|
expect(enemyPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
const enemyStartingHp = enemyPokemon.hp;
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.DRAGON_RAGE));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase, false);
|
||||||
|
|
||||||
|
expect(enemyPokemon.hp).toBe(enemyStartingHp - 80);
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability should not apply multiplier to counter moves",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.COUNTER]);
|
||||||
|
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE,Moves.TACKLE,Moves.TACKLE,Moves.TACKLE]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.CHARIZARD]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||||
|
expect(enemyPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
const playerStartingHp = leadPokemon.hp;
|
||||||
|
const enemyStartingHp = enemyPokemon.hp;
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.COUNTER));
|
||||||
|
await game.phaseInterceptor.to(DamagePhase);
|
||||||
|
|
||||||
|
const playerDamage = playerStartingHp - leadPokemon.hp;
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase, false);
|
||||||
|
|
||||||
|
expect(enemyPokemon.hp).toBe(enemyStartingHp - 4*playerDamage);
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability should not apply to multi-target moves",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
|
||||||
|
vi.spyOn(Overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(false);
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.EARTHQUAKE]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.CHARIZARD, Species.PIDGEOT]);
|
||||||
|
|
||||||
|
const playerPokemon = game.scene.getPlayerField();
|
||||||
|
expect(playerPokemon.length).toBe(2);
|
||||||
|
playerPokemon.forEach(p => expect(p).not.toBe(undefined));
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyField();
|
||||||
|
expect(enemyPokemon.length).toBe(2);
|
||||||
|
enemyPokemon.forEach(p => expect(p).not.toBe(undefined));
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.EARTHQUAKE));
|
||||||
|
await game.phaseInterceptor.to(CommandPhase);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 1, Moves.EARTHQUAKE));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase, false);
|
||||||
|
|
||||||
|
playerPokemon.forEach(p => expect(p.turnData.hitCount).toBe(1));
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability should apply to multi-target moves when hitting only one target",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.EARTHQUAKE]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.CHARIZARD]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||||
|
expect(enemyPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.EARTHQUAKE));
|
||||||
|
await game.phaseInterceptor.to(DamagePhase, false);
|
||||||
|
|
||||||
|
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability should only trigger post-target move effects once",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.MIND_BLOWN]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.PIDGEOT]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||||
|
expect(enemyPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.MIND_BLOWN));
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(DamagePhase, false);
|
||||||
|
|
||||||
|
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||||
|
|
||||||
|
// This test will time out if the user faints
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase, false);
|
||||||
|
|
||||||
|
expect(leadPokemon.hp).toBe(Math.floor(leadPokemon.getMaxHp()/2));
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"Burn Up only removes type after second strike with this ability",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.BURN_UP]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.CHARIZARD]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||||
|
expect(enemyPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.BURN_UP));
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(DamagePhase);
|
||||||
|
|
||||||
|
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||||
|
expect(enemyPokemon.hp).toBeGreaterThan(0);
|
||||||
|
expect(leadPokemon.isOfType(Type.FIRE)).toBe(true);
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase, false);
|
||||||
|
|
||||||
|
expect(leadPokemon.isOfType(Type.FIRE)).toBe(false);
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"Moves boosted by this ability and Multi-Lens should strike 4 times",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE]);
|
||||||
|
vi.spyOn(Overrides, "STARTING_HELD_ITEMS_OVERRIDE", "get").mockReturnValue([{name: "MULTI_LENS", count: 1}]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.CHARIZARD]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||||
|
expect(enemyPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.TACKLE));
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(DamagePhase);
|
||||||
|
|
||||||
|
expect(leadPokemon.turnData.hitCount).toBe(4);
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"Super Fang boosted by this ability and Multi-Lens should strike twice",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SUPER_FANG]);
|
||||||
|
vi.spyOn(Overrides, "STARTING_HELD_ITEMS_OVERRIDE", "get").mockReturnValue([{name: "MULTI_LENS", count: 1}]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.CHARIZARD]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||||
|
expect(enemyPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
const enemyStartingHp = enemyPokemon.hp;
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.SUPER_FANG));
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||||
|
vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true);
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(DamagePhase);
|
||||||
|
|
||||||
|
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
expect(enemyPokemon.hp).toBe(Math.ceil(enemyStartingHp * 0.25));
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"Seismic Toss boosted by this ability and Multi-Lens should strike twice",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SEISMIC_TOSS]);
|
||||||
|
vi.spyOn(Overrides, "STARTING_HELD_ITEMS_OVERRIDE", "get").mockReturnValue([{name: "MULTI_LENS", count: 1}]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.CHARIZARD]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||||
|
expect(enemyPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
const enemyStartingHp = enemyPokemon.hp;
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.SEISMIC_TOSS));
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||||
|
vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true);
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(DamagePhase);
|
||||||
|
|
||||||
|
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
expect(enemyPokemon.hp).toBe(enemyStartingHp - 200);
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"Hyper Beam boosted by this ability should strike twice, then recharge",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.HYPER_BEAM]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.CHARIZARD]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||||
|
expect(enemyPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.HYPER_BEAM));
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||||
|
vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true);
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(DamagePhase);
|
||||||
|
|
||||||
|
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||||
|
expect(leadPokemon.getTag(BattlerTagType.RECHARGING)).toBeUndefined();
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
expect(leadPokemon.getTag(BattlerTagType.RECHARGING)).toBeDefined();
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
/** TODO: Fix TRAPPED tag lapsing incorrectly, then run this test */
|
||||||
|
test.skip(
|
||||||
|
"Anchor Shot boosted by this ability should only trap the target after the second hit",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.ANCHOR_SHOT]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.CHARIZARD]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||||
|
expect(enemyPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.ANCHOR_SHOT));
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||||
|
vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true);
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(DamagePhase);
|
||||||
|
|
||||||
|
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||||
|
expect(enemyPokemon.getTag(BattlerTagType.TRAPPED)).toBeUndefined(); // Passes
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(MoveEndPhase);
|
||||||
|
expect(enemyPokemon.getTag(BattlerTagType.TRAPPED)).toBeDefined(); // Passes
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
expect(enemyPokemon.getTag(BattlerTagType.TRAPPED)).toBeDefined(); // Fails :(
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"Smack Down boosted by this ability should only ground the target after the second hit",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SMACK_DOWN]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.CHARIZARD]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||||
|
expect(enemyPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.SMACK_DOWN));
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||||
|
vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true);
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(DamagePhase);
|
||||||
|
|
||||||
|
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||||
|
expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeUndefined();
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined();
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"U-turn boosted by this ability should strike twice before forcing a switch",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.U_TURN]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||||
|
expect(enemyPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.U_TURN));
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||||
|
vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true);
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(MoveEffectPhase);
|
||||||
|
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||||
|
|
||||||
|
// This will cause this test to time out if the switch was forced on the first hit.
|
||||||
|
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"Wake-Up Slap boosted by this ability should only wake up the target after the second hit",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.WAKE_UP_SLAP]);
|
||||||
|
vi.spyOn(Overrides, "OPP_STATUS_OVERRIDE", "get").mockReturnValue(StatusEffect.SLEEP);
|
||||||
|
|
||||||
|
await game.startBattle([Species.CHARIZARD]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||||
|
expect(enemyPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.WAKE_UP_SLAP));
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||||
|
vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true);
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(DamagePhase);
|
||||||
|
|
||||||
|
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||||
|
expect(enemyPokemon.status?.effect).toBe(StatusEffect.SLEEP);
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
expect(enemyPokemon.status?.effect).toBeUndefined();
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability should not cause user to hit into King's Shield more than once",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE]);
|
||||||
|
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.KINGS_SHIELD,Moves.KINGS_SHIELD,Moves.KINGS_SHIELD,Moves.KINGS_SHIELD]);
|
||||||
|
|
||||||
|
await game.startBattle([Species.CHARIZARD]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||||
|
expect(enemyPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.TACKLE));
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase, false);
|
||||||
|
|
||||||
|
expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(-1);
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability should not cause user to hit into Storm Drain more than once",
|
||||||
|
async () => {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.WATER_GUN]);
|
||||||
|
vi.spyOn(Overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.STORM_DRAIN);
|
||||||
|
|
||||||
|
await game.startBattle([Species.CHARIZARD]);
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||||
|
expect(enemyPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, Moves.WATER_GUN));
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase, false);
|
||||||
|
|
||||||
|
expect(enemyPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(1);
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user