mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-01-30 04:37:14 +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 { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect";
|
||||
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 { Stat, getStatName } from "./pokemon-stat";
|
||||
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
|
||||
* For abilities that boost the base power of moves, see VariableMovePowerAbAttr
|
||||
@ -4632,7 +4710,7 @@ export function initAbilities() {
|
||||
new Ability(Abilities.AERILATE, 6)
|
||||
.attr(MoveTypeChangeAttr, Type.FLYING, 1.2, (user, target, move) => move.type === Type.NORMAL),
|
||||
new Ability(Abilities.PARENTAL_BOND, 6)
|
||||
.unimplemented(),
|
||||
.attr(AddSecondStrikeAbAttr, 0.25),
|
||||
new Ability(Abilities.DARK_AURA, 6)
|
||||
.attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => getPokemonMessage(pokemon, " is radiating a Dark Aura!"))
|
||||
.attr(FieldMoveTypePowerBoostAbAttr, Type.DARK, 4 / 3),
|
||||
|
@ -896,6 +896,13 @@ export class ProtectedTag extends BattlerTag {
|
||||
if (lapseType === BattlerTagLapseType.CUSTOM) {
|
||||
new CommonBattleAnim(CommonAnim.PROTECT, pokemon).play(pokemon.scene);
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -811,11 +811,14 @@ export class MoveEffectAttr extends MoveAttr {
|
||||
public trigger: MoveEffectTrigger;
|
||||
/** Should this effect only apply on the first hit? */
|
||||
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);
|
||||
this.trigger = trigger !== undefined ? trigger : MoveEffectTrigger.POST_APPLY;
|
||||
this.firstHitOnly = firstHitOnly;
|
||||
this.lastHitOnly = lastHitOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1064,7 +1067,7 @@ export class RecoilAttr extends MoveEffectAttr {
|
||||
private unblockable: boolean;
|
||||
|
||||
constructor(useHp: boolean = false, damageRatio: number = 0.25, unblockable: boolean = false) {
|
||||
super(true);
|
||||
super(true, MoveEffectTrigger.POST_APPLY, false, true);
|
||||
|
||||
this.useHp = useHp;
|
||||
this.damageRatio = damageRatio;
|
||||
@ -1085,8 +1088,8 @@ export class RecoilAttr extends MoveEffectAttr {
|
||||
return false;
|
||||
}
|
||||
|
||||
const recoilDamage = Math.max(Math.floor((!this.useHp ? user.turnData.currDamageDealt : user.getMaxHp()) * this.damageRatio),
|
||||
user.turnData.currDamageDealt ? 1 : 0);
|
||||
const recoilDamage = Math.max(Math.floor((!this.useHp ? user.turnData.damageDealt : user.getMaxHp()) * this.damageRatio),
|
||||
user.turnData.damageDealt ? 1 : 0);
|
||||
if (!recoilDamage) {
|
||||
return false;
|
||||
}
|
||||
@ -2013,7 +2016,7 @@ export class HealStatusEffectAttr extends MoveEffectAttr {
|
||||
* @param ...effects - List of status effects to cure
|
||||
*/
|
||||
constructor(selfTarget: boolean, ...effects: StatusEffect[]) {
|
||||
super(selfTarget);
|
||||
super(selfTarget, MoveEffectTrigger.POST_APPLY, false, true);
|
||||
|
||||
this.effects = effects;
|
||||
}
|
||||
@ -2770,7 +2773,7 @@ export class DoublePowerChanceAttr extends VariablePowerAttr {
|
||||
export abstract class ConsecutiveUsePowerMultiplierAttr extends MovePowerMultiplierAttr {
|
||||
constructor(limit: integer, resetOnFail: boolean, resetOnLimit?: boolean, ...comboMoves: Moves[]) {
|
||||
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 turnMove: TurnMove;
|
||||
@ -3139,8 +3142,13 @@ export class PunishmentPowerAttr extends VariablePowerAttr {
|
||||
|
||||
export class PresentPowerAttr extends VariablePowerAttr {
|
||||
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) {
|
||||
(args[0] as Utils.NumberHolder).value = 40;
|
||||
} else if (40 < powerSeed && powerSeed <= 70) {
|
||||
@ -3148,6 +3156,8 @@ export class PresentPowerAttr extends VariablePowerAttr {
|
||||
} else if (70 < powerSeed && powerSeed <= 80) {
|
||||
(args[0] as Utils.NumberHolder).value = 120;
|
||||
} 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(),
|
||||
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;
|
||||
private failOnOverlap: boolean;
|
||||
|
||||
constructor(tagType: BattlerTagType, selfTarget: boolean = false, failOnOverlap: boolean = false, turnCountMin: integer = 0, turnCountMax?: integer) {
|
||||
super(selfTarget);
|
||||
constructor(tagType: BattlerTagType, selfTarget: boolean = false, failOnOverlap: boolean = false, turnCountMin: integer = 0, turnCountMax?: integer, lastHitOnly: boolean = false) {
|
||||
super(selfTarget, MoveEffectTrigger.POST_APPLY, false, lastHitOnly);
|
||||
|
||||
this.tagType = tagType;
|
||||
this.turnCountMin = turnCountMin;
|
||||
@ -4071,7 +4081,7 @@ export class ConfuseAttr extends AddBattlerTagAttr {
|
||||
|
||||
export class RechargeAttr extends AddBattlerTagAttr {
|
||||
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;
|
||||
|
||||
constructor(user?: boolean, batonPass?: boolean) {
|
||||
super(false, MoveEffectTrigger.POST_APPLY, true);
|
||||
super(false, MoveEffectTrigger.POST_APPLY, false, true);
|
||||
this.user = !!user;
|
||||
this.batonPass = !!batonPass;
|
||||
}
|
||||
@ -4583,7 +4593,7 @@ export class RemoveTypeAttr extends MoveEffectAttr {
|
||||
private messageCallback: ((user: Pokemon) => void) | undefined;
|
||||
|
||||
constructor(removedType: Type, messageCallback?: (user: Pokemon) => void) {
|
||||
super(true, MoveEffectTrigger.POST_APPLY);
|
||||
super(true, MoveEffectTrigger.POST_TARGET);
|
||||
this.removedType = removedType;
|
||||
this.messageCallback = messageCallback;
|
||||
|
||||
@ -5391,7 +5401,7 @@ export class DiscourageFrequentUseAttr extends MoveAttr {
|
||||
|
||||
export class MoneyAttr extends MoveEffectAttr {
|
||||
constructor() {
|
||||
super(true, MoveEffectTrigger.HIT);
|
||||
super(true, MoveEffectTrigger.HIT, true);
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move): boolean {
|
||||
@ -6947,7 +6957,7 @@ export function initMoves() {
|
||||
.target(MoveTarget.BOTH_SIDES)
|
||||
.unimplemented(),
|
||||
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(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN])
|
||||
.attr(HitsTagAttr, BattlerTagType.FLYING, false)
|
||||
@ -7331,14 +7341,14 @@ export function initMoves() {
|
||||
.triageMove(),
|
||||
new AttackMove(Moves.THOUSAND_ARROWS, Type.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
|
||||
.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(AddBattlerTagAttr, BattlerTagType.INTERRUPTED)
|
||||
.attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN])
|
||||
.makesContact(false)
|
||||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||
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)
|
||||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||
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)
|
||||
.attr(ProtectAttr, BattlerTagType.BANEFUL_BUNKER),
|
||||
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),
|
||||
new AttackMove(Moves.DARKEST_LARIAT, Type.DARK, MoveCategory.PHYSICAL, 85, 100, 10, -1, 0, 7)
|
||||
.attr(IgnoreOpponentStatChangesAttr),
|
||||
@ -7524,7 +7534,7 @@ export function initMoves() {
|
||||
.attr(HealOnAllyAttr, 0.5, true, false)
|
||||
.ballBombMove(),
|
||||
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)
|
||||
.attr(TerrainChangeAttr, TerrainType.PSYCHIC)
|
||||
.target(MoveTarget.BOTH_SIDES),
|
||||
|
@ -23,7 +23,7 @@ import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HelpingHandTag
|
||||
import { WeatherType } from "../data/weather";
|
||||
import { TempBattleStat } from "../data/temp-battle-stat";
|
||||
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 { BattlerIndex } from "../battle";
|
||||
import { Mode } from "../ui/ui";
|
||||
@ -1800,6 +1800,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
}
|
||||
|
||||
if (cancelled.value) {
|
||||
source.stopMultiHit();
|
||||
result = HitResult.NO_EFFECT;
|
||||
} else {
|
||||
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(VariableDefAttr, source, this, move, targetDef);
|
||||
|
||||
const twoStrikeMultiplier = new Utils.NumberHolder(1);
|
||||
applyPreAttackAbAttrs(AddSecondStrikeAbAttr, source, this, move, 1, new Utils.IntegerHolder(0), twoStrikeMultiplier);
|
||||
|
||||
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 (!move.hasAttr(BypassBurnDamageReductionAttr)) {
|
||||
const burnDamageReductionCancelled = new Utils.BooleanHolder(false);
|
||||
@ -2251,6 +2255,18 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
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> {
|
||||
return new Promise(resolve => {
|
||||
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 { TempBattleStat } from "./data/temp-battle-stat";
|
||||
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 { getBiomeKey } from "./field/arena";
|
||||
import { BattleType, BattlerIndex, TurnCommand } from "./battle";
|
||||
@ -2878,6 +2878,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
const hitCount = new Utils.IntegerHolder(1);
|
||||
// Assume single target for multi hit
|
||||
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)) {
|
||||
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 };
|
||||
user.pushMoveHistory(moveHistoryEntry);
|
||||
|
||||
const targetHitChecks = Object.fromEntries(targets.map(p => [p.getBattlerIndex(), this.hitCheck(p)]));
|
||||
const activeTargets = targets.map(t => t.isActive(true));
|
||||
@ -2900,6 +2900,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
this.scene.queueMessage(i18next.t("battle:attackFailed"));
|
||||
moveHistoryEntry.result = MoveResult.FAIL;
|
||||
}
|
||||
user.pushMoveHistory(moveHistoryEntry);
|
||||
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 firstHit = moveHistoryEntry.result !== MoveResult.SUCCESS;
|
||||
const firstHit = (user.turnData.hitsLeft === user.turnData.hitCount);
|
||||
|
||||
if (firstHit) {
|
||||
user.pushMoveHistory(moveHistoryEntry);
|
||||
}
|
||||
|
||||
moveHistoryEntry.result = MoveResult.SUCCESS;
|
||||
|
||||
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 => {
|
||||
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(() => {
|
||||
if (hitResult !== HitResult.FAIL) {
|
||||
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
|
||||
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) {
|
||||
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)) {
|
||||
const flinched = new Utils.BooleanHolder(false);
|
||||
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);
|
||||
}
|
||||
}
|
||||
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(() => {
|
||||
return Utils.executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult).then(() => {
|
||||
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
|
||||
const postTarget = applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET,
|
||||
user, null, move);
|
||||
// Trigger effect which should only apply one time on the last hit after all targeted effects have already applied
|
||||
const postTarget = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive()) ?
|
||||
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
|
||||
applyAttrs[applyAttrs.length - 1]?.then(() => postTarget);
|
||||
} else { // Otherwise, push a new asynchronous move effect
|
||||
applyAttrs.push(postTarget);
|
||||
if (!!postTarget) {
|
||||
if (applyAttrs.length) { // If there is a pending asynchronous move effect, do this after
|
||||
applyAttrs[applyAttrs.length - 1]?.then(() => postTarget);
|
||||
} else { // Otherwise, push a new asynchronous move effect
|
||||
applyAttrs.push(postTarget);
|
||||
}
|
||||
}
|
||||
|
||||
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