mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-01-17 22:40:59 +00:00
[Move] Refactor Conditional Protection and Protect interactions (#2731)
* Refactor protection moves * Revert Protected move result change * Fix typo in documentation * Fix merge conflicts in integration tests * Improve readability of apply function * Fix effects applying through Protect * Fix typos in `src/data/egg.ts` comments * [Localization(it)] Added Honey Gather translation (#2301) * Update ability.ts [Localization(it)] * Update ability.ts [Localization(it)] Fixed capitalization * Hitting into protect now records a move as failed * Add check for move success before applying additional effects (Stone Axe) * `Overrides` should be capitalized * Add fail conditions for `RechargeAttr` and Jaw Lock * Update protect test * Add check for protect in Glaive Rush * Update tests to use new overrides helpers * Account for Mat Block and add protect test for Glaive Rush --------- Co-authored-by: Niccolò <123510358+NicusPulcis@users.noreply.github.com> * Crafty Shield now blocks ALL protect-ignoring moves * Crafty Shield test + Fix blocking ally-target moves * ESLint * Mat Block and Wide Guard integration tests * Fix strict-null errors in integration tests * Fix strict-null issue in arena-tag * Fix strict-null in unrelated test :pikamad: * Fix conflicting AbAttr --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: Niccolò <123510358+NicusPulcis@users.noreply.github.com>
This commit is contained in:
parent
161043ecd6
commit
a760022c77
@ -1,13 +1,13 @@
|
||||
import { Arena } from "../field/arena";
|
||||
import { Type } from "./type";
|
||||
import * as Utils from "../utils";
|
||||
import { MoveCategory, allMoves, MoveTarget } from "./move";
|
||||
import { MoveCategory, allMoves, MoveTarget, IncrementMovePriorityAttr, applyMoveAttrs } from "./move";
|
||||
import { getPokemonNameWithAffix } from "../messages";
|
||||
import Pokemon, { HitResult, PokemonMove } from "../field/pokemon";
|
||||
import { MoveEffectPhase, PokemonHealPhase, ShowAbilityPhase, StatChangePhase } from "../phases";
|
||||
import { StatusEffect } from "./status-effect";
|
||||
import { BattlerIndex } from "../battle";
|
||||
import { BlockNonDirectDamageAbAttr, ProtectStatAbAttr, applyAbAttrs } from "./ability";
|
||||
import { BlockNonDirectDamageAbAttr, ChangeMovePriorityAbAttr, ProtectStatAbAttr, applyAbAttrs } from "./ability";
|
||||
import { BattleStat } from "./battle-stat";
|
||||
import { CommonAnim, CommonBattleAnim } from "./battle-anims";
|
||||
import i18next from "i18next";
|
||||
@ -186,20 +186,23 @@ class AuroraVeilTag extends WeakenMoveScreenTag {
|
||||
}
|
||||
}
|
||||
|
||||
type ProtectConditionFunc = (...args: any[]) => boolean;
|
||||
type ProtectConditionFunc = (arena: Arena, moveId: Moves) => boolean;
|
||||
|
||||
/**
|
||||
* Abstract class to implement conditional team protection
|
||||
* Class to implement conditional team protection
|
||||
* applies protection based on the attributes of incoming moves
|
||||
* @param protectConditionFunc: The function determining if an incoming move is negated
|
||||
*/
|
||||
abstract class ConditionalProtectTag extends ArenaTag {
|
||||
export class ConditionalProtectTag extends ArenaTag {
|
||||
/** The condition function to determine which moves are negated */
|
||||
protected protectConditionFunc: ProtectConditionFunc;
|
||||
/** Does this apply to all moves, including those that ignore other forms of protection? */
|
||||
protected ignoresBypass: boolean;
|
||||
|
||||
constructor(tagType: ArenaTagType, sourceMove: Moves, sourceId: integer, side: ArenaTagSide, condition: ProtectConditionFunc) {
|
||||
constructor(tagType: ArenaTagType, sourceMove: Moves, sourceId: integer, side: ArenaTagSide, condition: ProtectConditionFunc, ignoresBypass: boolean = false) {
|
||||
super(tagType, 1, sourceMove, sourceId, side);
|
||||
|
||||
this.protectConditionFunc = condition;
|
||||
this.ignoresBypass = ignoresBypass;
|
||||
}
|
||||
|
||||
onAdd(arena: Arena): void {
|
||||
@ -213,42 +216,91 @@ abstract class ConditionalProtectTag extends ArenaTag {
|
||||
* apply(): Checks incoming moves against the condition function
|
||||
* and protects the target if conditions are met
|
||||
* @param arena The arena containing this tag
|
||||
* @param args[0] (Utils.BooleanHolder) Signals if the move is cancelled
|
||||
* @param args[1] (Pokemon) The intended target of the move
|
||||
* @param args[2...] (any[]) The parameters to the condition function
|
||||
* @param args\[0\] (Utils.BooleanHolder) Signals if the move is cancelled
|
||||
* @param args\[1\] (Pokemon) The Pokemon using the move
|
||||
* @param args\[2\] (Pokemon) The intended target of the move
|
||||
* @param args\[3\] (Moves) The parameters to the condition function
|
||||
* @param args\[4\] (Utils.BooleanHolder) Signals if the applied protection supercedes protection-ignoring effects
|
||||
* @returns
|
||||
*/
|
||||
apply(arena: Arena, args: any[]): boolean {
|
||||
if ((args[0] as Utils.BooleanHolder).value) {
|
||||
return false;
|
||||
}
|
||||
const [ cancelled, user, target, moveId, ignoresBypass ] = args;
|
||||
|
||||
const target = args[1] as Pokemon;
|
||||
if ((this.side === ArenaTagSide.PLAYER) === target.isPlayer()
|
||||
&& this.protectConditionFunc(...args.slice(2))) {
|
||||
(args[0] as Utils.BooleanHolder).value = true;
|
||||
new CommonBattleAnim(CommonAnim.PROTECT, target).play(arena.scene);
|
||||
arena.scene.queueMessage(i18next.t("arenaTag:conditionalProtectApply", { moveName: super.getMoveName(), pokemonNameWithAffix: getPokemonNameWithAffix(target) }));
|
||||
return true;
|
||||
if (cancelled instanceof Utils.BooleanHolder
|
||||
&& user instanceof Pokemon
|
||||
&& target instanceof Pokemon
|
||||
&& typeof moveId === "number"
|
||||
&& ignoresBypass instanceof Utils.BooleanHolder) {
|
||||
|
||||
if ((this.side === ArenaTagSide.PLAYER) === target.isPlayer()
|
||||
&& this.protectConditionFunc(arena, moveId)) {
|
||||
if (!cancelled.value) {
|
||||
cancelled.value = true;
|
||||
user.stopMultiHit(target);
|
||||
|
||||
new CommonBattleAnim(CommonAnim.PROTECT, target).play(arena.scene);
|
||||
arena.scene.queueMessage(i18next.t("arenaTag:conditionalProtectApply", { moveName: super.getMoveName(), pokemonNameWithAffix: getPokemonNameWithAffix(target) }));
|
||||
}
|
||||
|
||||
ignoresBypass.value = ignoresBypass.value || this.ignoresBypass;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Condition function for {@link https://bulbapedia.bulbagarden.net/wiki/Quick_Guard_(move) Quick Guard's}
|
||||
* protection effect.
|
||||
* @param arena {@linkcode Arena} The arena containing the protection effect
|
||||
* @param moveId {@linkcode Moves} The move to check against this condition
|
||||
* @returns `true` if the incoming move's priority is greater than 0. This includes
|
||||
* moves with modified priorities from abilities (e.g. Prankster)
|
||||
*/
|
||||
const QuickGuardConditionFunc: ProtectConditionFunc = (arena, moveId) => {
|
||||
const move = allMoves[moveId];
|
||||
const priority = new Utils.IntegerHolder(move.priority);
|
||||
const effectPhase = arena.scene.getCurrentPhase();
|
||||
|
||||
if (effectPhase instanceof MoveEffectPhase) {
|
||||
const attacker = effectPhase.getUserPokemon()!;
|
||||
applyMoveAttrs(IncrementMovePriorityAttr, attacker, null, move, priority);
|
||||
applyAbAttrs(ChangeMovePriorityAbAttr, attacker, null, move, priority);
|
||||
}
|
||||
return priority.value > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Quick_Guard_(move) Quick Guard}
|
||||
* Condition: The incoming move has increased priority.
|
||||
*/
|
||||
class QuickGuardTag extends ConditionalProtectTag {
|
||||
constructor(sourceId: integer, side: ArenaTagSide) {
|
||||
super(ArenaTagType.QUICK_GUARD, Moves.QUICK_GUARD, sourceId, side,
|
||||
(priority: integer) : boolean => {
|
||||
return priority > 0;
|
||||
}
|
||||
);
|
||||
super(ArenaTagType.QUICK_GUARD, Moves.QUICK_GUARD, sourceId, side, QuickGuardConditionFunc);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Condition function for {@link https://bulbapedia.bulbagarden.net/wiki/Wide_Guard_(move) Wide Guard's}
|
||||
* protection effect.
|
||||
* @param arena {@linkcode Arena} The arena containing the protection effect
|
||||
* @param moveId {@linkcode Moves} The move to check against this condition
|
||||
* @returns `true` if the incoming move is multi-targeted (even if it's only used against one Pokemon).
|
||||
*/
|
||||
const WideGuardConditionFunc: ProtectConditionFunc = (arena, moveId) : boolean => {
|
||||
const move = allMoves[moveId];
|
||||
|
||||
switch (move.moveTarget) {
|
||||
case MoveTarget.ALL_ENEMIES:
|
||||
case MoveTarget.ALL_NEAR_ENEMIES:
|
||||
case MoveTarget.ALL_OTHERS:
|
||||
case MoveTarget.ALL_NEAR_OTHERS:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Wide_Guard_(move) Wide Guard}
|
||||
* Condition: The incoming move can target multiple Pokemon. The move's source
|
||||
@ -256,32 +308,29 @@ class QuickGuardTag extends ConditionalProtectTag {
|
||||
*/
|
||||
class WideGuardTag extends ConditionalProtectTag {
|
||||
constructor(sourceId: integer, side: ArenaTagSide) {
|
||||
super(ArenaTagType.WIDE_GUARD, Moves.WIDE_GUARD, sourceId, side,
|
||||
(moveTarget: MoveTarget) : boolean => {
|
||||
switch (moveTarget) {
|
||||
case MoveTarget.ALL_ENEMIES:
|
||||
case MoveTarget.ALL_NEAR_ENEMIES:
|
||||
case MoveTarget.ALL_OTHERS:
|
||||
case MoveTarget.ALL_NEAR_OTHERS:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
);
|
||||
super(ArenaTagType.WIDE_GUARD, Moves.WIDE_GUARD, sourceId, side, WideGuardConditionFunc);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Condition function for {@link https://bulbapedia.bulbagarden.net/wiki/Mat_Block_(move) Mat Block's}
|
||||
* protection effect.
|
||||
* @param arena {@linkcode Arena} The arena containing the protection effect.
|
||||
* @param moveId {@linkcode Moves} The move to check against this condition.
|
||||
* @returns `true` if the incoming move is not a Status move.
|
||||
*/
|
||||
const MatBlockConditionFunc: ProtectConditionFunc = (arena, moveId) : boolean => {
|
||||
const move = allMoves[moveId];
|
||||
return move.category !== MoveCategory.STATUS;
|
||||
};
|
||||
|
||||
/**
|
||||
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Mat_Block_(move) Mat Block}
|
||||
* Condition: The incoming move is a Physical or Special attack move.
|
||||
*/
|
||||
class MatBlockTag extends ConditionalProtectTag {
|
||||
constructor(sourceId: integer, side: ArenaTagSide) {
|
||||
super(ArenaTagType.MAT_BLOCK, Moves.MAT_BLOCK, sourceId, side,
|
||||
(moveCategory: MoveCategory) : boolean => {
|
||||
return moveCategory !== MoveCategory.STATUS;
|
||||
}
|
||||
);
|
||||
super(ArenaTagType.MAT_BLOCK, Moves.MAT_BLOCK, sourceId, side, MatBlockConditionFunc);
|
||||
}
|
||||
|
||||
onAdd(arena: Arena) {
|
||||
@ -296,6 +345,22 @@ class MatBlockTag extends ConditionalProtectTag {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Condition function for {@link https://bulbapedia.bulbagarden.net/wiki/Crafty_Shield_(move) Crafty Shield's}
|
||||
* protection effect.
|
||||
* @param arena {@linkcode Arena} The arena containing the protection effect
|
||||
* @param moveId {@linkcode Moves} The move to check against this condition
|
||||
* @returns `true` if the incoming move is a Status move, is not a hazard, and does not target all
|
||||
* Pokemon or sides of the field.
|
||||
*/
|
||||
const CraftyShieldConditionFunc: ProtectConditionFunc = (arena, moveId) => {
|
||||
const move = allMoves[moveId];
|
||||
return move.category === MoveCategory.STATUS
|
||||
&& move.moveTarget !== MoveTarget.ENEMY_SIDE
|
||||
&& move.moveTarget !== MoveTarget.BOTH_SIDES
|
||||
&& move.moveTarget !== MoveTarget.ALL;
|
||||
};
|
||||
|
||||
/**
|
||||
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Crafty_Shield_(move) Crafty Shield}
|
||||
* Condition: The incoming move is a Status move, is not a hazard, and does
|
||||
@ -303,14 +368,7 @@ class MatBlockTag extends ConditionalProtectTag {
|
||||
*/
|
||||
class CraftyShieldTag extends ConditionalProtectTag {
|
||||
constructor(sourceId: integer, side: ArenaTagSide) {
|
||||
super(ArenaTagType.CRAFTY_SHIELD, Moves.CRAFTY_SHIELD, sourceId, side,
|
||||
(moveCategory: MoveCategory, moveTarget: MoveTarget) : boolean => {
|
||||
return moveCategory === MoveCategory.STATUS
|
||||
&& moveTarget !== MoveTarget.ENEMY_SIDE
|
||||
&& moveTarget !== MoveTarget.BOTH_SIDES
|
||||
&& moveTarget !== MoveTarget.ALL;
|
||||
}
|
||||
);
|
||||
super(ArenaTagType.CRAFTY_SHIELD, Moves.CRAFTY_SHIELD, sourceId, side, CraftyShieldConditionFunc, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4298,15 +4298,25 @@ export class AddBattlerTagAttr extends MoveEffectAttr {
|
||||
public tagType: BattlerTagType;
|
||||
public turnCountMin: integer;
|
||||
public turnCountMax: integer;
|
||||
protected cancelOnFail: boolean;
|
||||
private failOnOverlap: boolean;
|
||||
|
||||
constructor(tagType: BattlerTagType, selfTarget: boolean = false, failOnOverlap: boolean = false, turnCountMin: integer = 0, turnCountMax?: integer, lastHitOnly: boolean = false) {
|
||||
constructor(tagType: BattlerTagType, selfTarget: boolean = false, failOnOverlap: boolean = false, turnCountMin: integer = 0, turnCountMax?: integer, lastHitOnly: boolean = false, cancelOnFail: boolean = false) {
|
||||
super(selfTarget, MoveEffectTrigger.POST_APPLY, false, lastHitOnly);
|
||||
|
||||
this.tagType = tagType;
|
||||
this.turnCountMin = turnCountMin;
|
||||
this.turnCountMax = turnCountMax !== undefined ? turnCountMax : turnCountMin;
|
||||
this.failOnOverlap = !!failOnOverlap;
|
||||
this.cancelOnFail = cancelOnFail;
|
||||
}
|
||||
|
||||
canApply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (!super.canApply(user, target, move, args) || (this.cancelOnFail === true && user.getLastXMoves(1)[0].result === MoveResult.FAIL)) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
@ -4505,7 +4515,7 @@ export class ConfuseAttr extends AddBattlerTagAttr {
|
||||
|
||||
export class RechargeAttr extends AddBattlerTagAttr {
|
||||
constructor() {
|
||||
super(BattlerTagType.RECHARGING, true, false, 1, 1, true);
|
||||
super(BattlerTagType.RECHARGING, true, false, 1, 1, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4690,7 +4700,7 @@ export class AddArenaTrapTagHitAttr extends AddArenaTagAttr {
|
||||
const moveChance = this.getMoveChance(user,target,move,this.selfTarget, true);
|
||||
const side = (this.selfSideTarget ? user : target).isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||
const tag = user.scene.arena.getTagOnSide(this.tagType, side) as ArenaTrapTag;
|
||||
if ((moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance)) {
|
||||
if ((moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) && user.getLastXMoves(1)[0].result === MoveResult.SUCCESS) {
|
||||
user.scene.arena.addTag(this.tagType, 0, move.id, user.id, side);
|
||||
if (!tag) {
|
||||
return true;
|
||||
@ -8295,8 +8305,8 @@ export function initMoves() {
|
||||
.attr(HighCritAttr)
|
||||
.attr(BypassRedirectAttr),
|
||||
new AttackMove(Moves.JAW_LOCK, Type.DARK, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 8)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, true, false, 1)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1, 1, false, true)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, true, false, 1, 1, false, true)
|
||||
.bitingMove(),
|
||||
new SelfStatusMove(Moves.STUFF_CHEEKS, Type.NORMAL, -1, 10, -1, 0, 8) // TODO: Stuff Cheeks should not be selectable when the user does not have a berry, see wiki
|
||||
.attr(EatBerryAttr)
|
||||
@ -8772,7 +8782,10 @@ export function initMoves() {
|
||||
.attr(ClearTerrainAttr),
|
||||
new AttackMove(Moves.GLAIVE_RUSH, Type.DRAGON, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 9)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_GET_HIT, true, false, 0, 0, true)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.RECEIVE_DOUBLE_DAMAGE, true, false, 0, 0, true),
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.RECEIVE_DOUBLE_DAMAGE, true, false, 0, 0, true)
|
||||
.condition((user, target, move) => {
|
||||
return !(target.getTag(BattlerTagType.PROTECTED)?.tagType === "PROTECTED" || target.scene.arena.getTag(ArenaTagType.MAT_BLOCK)?.tagType === "MAT_BLOCK");
|
||||
}),
|
||||
new StatusMove(Moves.REVIVAL_BLESSING, Type.NORMAL, -1, 1, -1, 0, 9)
|
||||
.triageMove()
|
||||
.attr(RevivalBlessingAttr)
|
||||
|
@ -3,7 +3,7 @@ import BattleScene, { AnySound } from "../battle-scene";
|
||||
import { Variant, VariantSet, variantColorCache } from "#app/data/variant";
|
||||
import { variantData } from "#app/data/variant";
|
||||
import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "../ui/battle-info";
|
||||
import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, StatusMoveTypeImmunityAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatChangesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, MoveFlags, NeutralDamageAgainstFlyingTypeMultiplierAttr, OneHitKOAccuracyAttr } from "../data/move";
|
||||
import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, StatusMoveTypeImmunityAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatChangesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, OneHitKOAccuracyAttr } from "../data/move";
|
||||
import { default as PokemonSpecies, PokemonSpeciesForm, SpeciesFormKey, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm, getStarterValueFriendshipCap, speciesStarters, starterPassiveAbilities } from "../data/pokemon-species";
|
||||
import { Constructor } from "#app/utils";
|
||||
import * as Utils from "../utils";
|
||||
@ -1942,20 +1942,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
typeMultiplier.value = 0;
|
||||
}
|
||||
|
||||
// Apply arena tags for conditional protection
|
||||
if (!move.checkFlag(MoveFlags.IGNORE_PROTECT, source, this) && !move.isAllyTarget()) {
|
||||
this.scene.arena.applyTagsForSide(ArenaTagType.QUICK_GUARD, defendingSide, cancelled, this, move.priority);
|
||||
this.scene.arena.applyTagsForSide(ArenaTagType.WIDE_GUARD, defendingSide, cancelled, this, move.moveTarget);
|
||||
this.scene.arena.applyTagsForSide(ArenaTagType.MAT_BLOCK, defendingSide, cancelled, this, move.category);
|
||||
this.scene.arena.applyTagsForSide(ArenaTagType.CRAFTY_SHIELD, defendingSide, cancelled, this, move.category, move.moveTarget);
|
||||
}
|
||||
|
||||
// Apply exceptional condition of Crafty Shield if the move used is Curse
|
||||
if (move.id === Moves.CURSE) {
|
||||
const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||
this.scene.arena.applyTagsForSide(ArenaTagType.CRAFTY_SHIELD, defendingSide, cancelled, this, move.category, move.moveTarget);
|
||||
}
|
||||
|
||||
switch (moveCategory) {
|
||||
case MoveCategory.PHYSICAL:
|
||||
case MoveCategory.SPECIAL:
|
||||
|
@ -24,7 +24,7 @@ import { getPokemonNameWithAffix } from "./messages";
|
||||
import { Starter } from "./ui/starter-select-ui-handler";
|
||||
import { Gender } from "./data/gender";
|
||||
import { Weather, WeatherType, getRandomWeatherType, getTerrainBlockMessage, getWeatherDamageMessage, getWeatherLapseMessage } from "./data/weather";
|
||||
import { ArenaTagSide, ArenaTrapTag, MistTag, TrickRoomTag } from "./data/arena-tag";
|
||||
import { ArenaTagSide, ArenaTrapTag, ConditionalProtectTag, MistTag, TrickRoomTag } from "./data/arena-tag";
|
||||
import { CheckTrappedAbAttr, 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, ChangeMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, PreventBypassSpeedChanceAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, PokemonTypeChangeAbAttr, applyPreAttackAbAttrs, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr, MaxMultiHitAbAttr, HealFromBerryUseAbAttr, IgnoreMoveEffectsAbAttr, BlockStatusDamageAbAttr, BypassSpeedChanceAbAttr, AddSecondStrikeAbAttr } from "./data/ability";
|
||||
import { Unlockables, getUnlockableName } from "./system/unlockables";
|
||||
import { getBiomeKey } from "./field/arena";
|
||||
@ -3038,8 +3038,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
/**
|
||||
* Log to be entered into the user's move history once the move result is resolved.
|
||||
* Note that `result` (a {@linkcode MoveResult}) logs whether the move was successfully
|
||||
* used in the sense of it not failing or missing; it does not account for the move's
|
||||
* effectiveness (which is logged as a {@linkcode HitResult}).
|
||||
* used in the sense of "Does it have an effect on the user?".
|
||||
*/
|
||||
const moveHistoryEntry = { move: this.move.moveId, targets: this.targets, result: MoveResult.PENDING, virtual: this.move.virtual };
|
||||
|
||||
@ -3091,8 +3090,20 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
continue;
|
||||
}
|
||||
|
||||
/** Is the invoked move blocked by a protection effect on the target? */
|
||||
const isProtected = !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target) && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType));
|
||||
/** The {@linkcode ArenaTagSide} to which the target belongs */
|
||||
const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||
/** Has the invoked move been cancelled by conditional protection (e.g Quick Guard)? */
|
||||
const hasConditionalProtectApplied = new Utils.BooleanHolder(false);
|
||||
/** Does the applied conditional protection bypass Protect-ignoring effects? */
|
||||
const bypassIgnoreProtect = new Utils.BooleanHolder(false);
|
||||
// If the move is not targeting a Pokemon on the user's side, try to apply conditional protection effects
|
||||
if (!this.move.getMove().isAllyTarget()) {
|
||||
this.scene.arena.applyTagsForSide(ConditionalProtectTag, targetSide, hasConditionalProtectApplied, user, target, move.id, bypassIgnoreProtect);
|
||||
}
|
||||
|
||||
/** Is the target protected by Protect, etc. or a relevant conditional protection effect? */
|
||||
const isProtected = (bypassIgnoreProtect.value || !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target))
|
||||
&& (hasConditionalProtectApplied.value || target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType)));
|
||||
|
||||
/** Does this phase represent the invoked move's first strike? */
|
||||
const firstHit = (user.turnData.hitsLeft === user.turnData.hitCount);
|
||||
@ -3104,8 +3115,8 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
|
||||
/**
|
||||
* Since all fail/miss checks have applied, the move is considered successfully applied.
|
||||
* It's worth noting that if the move has no effect or is protected against, it's move
|
||||
* result is still logged as a SUCCESS.
|
||||
* It's worth noting that if the move has no effect or is protected against, this assignment
|
||||
* is overwritten and the move is logged as a FAIL.
|
||||
*/
|
||||
moveHistoryEntry.result = MoveResult.SUCCESS;
|
||||
|
||||
@ -3136,6 +3147,14 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
hasHit = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the move has no effect on the target (i.e. the target is protected or immune),
|
||||
* change the logged move result to FAIL.
|
||||
*/
|
||||
if (hitResult === HitResult.NO_EFFECT) {
|
||||
moveHistoryEntry.result = MoveResult.FAIL;
|
||||
}
|
||||
|
||||
/** Does this phase represent the invoked move's last strike? */
|
||||
const lastHit = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive());
|
||||
|
||||
|
124
src/test/moves/crafty_shield.test.ts
Normal file
124
src/test/moves/crafty_shield.test.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
||||
import GameManager from "../utils/gameManager";
|
||||
import { Species } from "#enums/species";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { getMovePosition } from "../utils/gameManagerUtils";
|
||||
import { BerryPhase, CommandPhase } from "#app/phases.js";
|
||||
import { BattleStat } from "#app/data/battle-stat.js";
|
||||
import { BattlerTagType } from "#app/enums/battler-tag-type.js";
|
||||
|
||||
const TIMEOUT = 20 * 1000;
|
||||
|
||||
describe("Moves - Crafty Shield", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
|
||||
game.override.battleType("double");
|
||||
|
||||
game.override.moveset([Moves.CRAFTY_SHIELD, Moves.SPLASH, Moves.SWORDS_DANCE]);
|
||||
|
||||
game.override.enemySpecies(Species.SNORLAX);
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.GROWL));
|
||||
game.override.enemyAbility(Abilities.INSOMNIA);
|
||||
|
||||
game.override.startingLevel(100);
|
||||
game.override.enemyLevel(100);
|
||||
});
|
||||
|
||||
test(
|
||||
"should protect the user and allies from status moves",
|
||||
async () => {
|
||||
await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerField();
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.CRAFTY_SHIELD));
|
||||
|
||||
await game.phaseInterceptor.to(CommandPhase);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
leadPokemon.forEach(p => expect(p.summonData.battleStats[BattleStat.ATK]).toBe(0));
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"should not protect the user and allies from attack moves",
|
||||
async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
|
||||
|
||||
await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerField();
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.CRAFTY_SHIELD));
|
||||
|
||||
await game.phaseInterceptor.to(CommandPhase);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
expect(leadPokemon.some(p => p.hp < p.getMaxHp())).toBeTruthy();
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"should protect the user and allies from moves that ignore other protection",
|
||||
async () => {
|
||||
game.override.enemySpecies(Species.DUSCLOPS);
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.CURSE));
|
||||
|
||||
await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerField();
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.CRAFTY_SHIELD));
|
||||
|
||||
await game.phaseInterceptor.to(CommandPhase);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
leadPokemon.forEach(p => expect(p.getTag(BattlerTagType.CURSED)).toBeUndefined());
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"should not block allies' self-targeted moves",
|
||||
async () => {
|
||||
await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerField();
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.CRAFTY_SHIELD));
|
||||
|
||||
await game.phaseInterceptor.to(CommandPhase);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 1, Moves.SWORDS_DANCE));
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
expect(leadPokemon[0].summonData.battleStats[BattleStat.ATK]).toBe(0);
|
||||
expect(leadPokemon[1].summonData.battleStats[BattleStat.ATK]).toBe(2);
|
||||
}
|
||||
);
|
||||
});
|
@ -128,4 +128,28 @@ describe("Moves - Glaive Rush", () => {
|
||||
expect(player.hp).toBe(player.getMaxHp());
|
||||
|
||||
}, 20000);
|
||||
|
||||
it("secondary effects don't activate if move fails", async() => {
|
||||
game.override.moveset([Moves.SHADOW_SNEAK, Moves.PROTECT, Moves.SPLASH, Moves.GLAIVE_RUSH]);
|
||||
await game.startBattle();
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
enemy.hp = 1000;
|
||||
player.hp = 1000;
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.PROTECT));
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.SHADOW_SNEAK));
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.SPLASH));
|
||||
const damagedHP1 = 1000 - enemy.hp;
|
||||
enemy.hp = 1000;
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.SHADOW_SNEAK));
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
const damagedHP2 = 1000 - enemy.hp;
|
||||
|
||||
expect(damagedHP2).toBeGreaterThanOrEqual((damagedHP1 * 2) - 1);
|
||||
}, 20000);
|
||||
});
|
||||
|
107
src/test/moves/mat_block.test.ts
Normal file
107
src/test/moves/mat_block.test.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
||||
import GameManager from "../utils/gameManager";
|
||||
import { Species } from "#enums/species";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { getMovePosition } from "../utils/gameManagerUtils";
|
||||
import { BerryPhase, CommandPhase, TurnEndPhase } from "#app/phases.js";
|
||||
import { BattleStat } from "#app/data/battle-stat.js";
|
||||
|
||||
const TIMEOUT = 20 * 1000;
|
||||
|
||||
describe("Moves - Mat Block", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
|
||||
game.override.battleType("double");
|
||||
|
||||
game.override.moveset([Moves.MAT_BLOCK, Moves.SPLASH]);
|
||||
|
||||
game.override.enemySpecies(Species.SNORLAX);
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
|
||||
game.override.enemyAbility(Abilities.INSOMNIA);
|
||||
|
||||
game.override.startingLevel(100);
|
||||
game.override.enemyLevel(100);
|
||||
});
|
||||
|
||||
test(
|
||||
"should protect the user and allies from attack moves",
|
||||
async () => {
|
||||
await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerField();
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.MAT_BLOCK));
|
||||
|
||||
await game.phaseInterceptor.to(CommandPhase);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
leadPokemon.forEach(p => expect(p.hp).toBe(p.getMaxHp()));
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"should not protect the user and allies from status moves",
|
||||
async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.GROWL));
|
||||
|
||||
await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerField();
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.MAT_BLOCK));
|
||||
|
||||
await game.phaseInterceptor.to(CommandPhase);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
leadPokemon.forEach(p => expect(p.summonData.battleStats[BattleStat.ATK]).toBe(-2));
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"should fail when used after the first turn",
|
||||
async () => {
|
||||
await game.startBattle([Species.BLASTOISE, Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerField();
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH));
|
||||
await game.phaseInterceptor.to(CommandPhase);
|
||||
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
const leadStartingHp = leadPokemon.map(p => p.hp);
|
||||
|
||||
await game.phaseInterceptor.to(CommandPhase, false);
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.MAT_BLOCK));
|
||||
await game.phaseInterceptor.to(CommandPhase);
|
||||
game.doAttack(getMovePosition(game.scene, 1, Moves.MAT_BLOCK));
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
expect(leadPokemon.some((p, i) => p.hp < leadStartingHp[i])).toBeTruthy();
|
||||
}, TIMEOUT
|
||||
);
|
||||
});
|
114
src/test/moves/protect.test.ts
Normal file
114
src/test/moves/protect.test.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import GameManager from "../utils/gameManager";
|
||||
import { Species } from "#enums/species";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { getMovePosition } from "../utils/gameManagerUtils";
|
||||
import { BerryPhase } from "#app/phases.js";
|
||||
import { BattleStat } from "#app/data/battle-stat.js";
|
||||
import { allMoves } from "#app/data/move.js";
|
||||
import { ArenaTagSide, ArenaTrapTag } from "#app/data/arena-tag.js";
|
||||
|
||||
const TIMEOUT = 20 * 1000;
|
||||
|
||||
describe("Moves - Protect", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
|
||||
game.override.battleType("single");
|
||||
|
||||
game.override.moveset([Moves.PROTECT]);
|
||||
game.override.enemySpecies(Species.SNORLAX);
|
||||
|
||||
game.override.enemyAbility(Abilities.INSOMNIA);
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
|
||||
|
||||
game.override.startingLevel(100);
|
||||
game.override.enemyLevel(100);
|
||||
});
|
||||
|
||||
test(
|
||||
"should protect the user from attacks",
|
||||
async () => {
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.PROTECT));
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"should prevent secondary effects from the opponent's attack",
|
||||
async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.CEASELESS_EDGE));
|
||||
vi.spyOn(allMoves[Moves.CEASELESS_EDGE], "accuracy", "get").mockReturnValue(100);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.PROTECT));
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
|
||||
expect(game.scene.arena.getTagOnSide(ArenaTrapTag, ArenaTagSide.ENEMY)).toBeUndefined();
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"should protect the user from status moves",
|
||||
async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.CHARM));
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.PROTECT));
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(0);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"should stop subsequent hits of a multi-hit move",
|
||||
async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.TACHYON_CUTTER));
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.PROTECT));
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
|
||||
expect(enemyPokemon.turnData.hitCount).toBe(1);
|
||||
}, TIMEOUT
|
||||
);
|
||||
});
|
105
src/test/moves/quick_guard.test.ts
Normal file
105
src/test/moves/quick_guard.test.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
||||
import GameManager from "../utils/gameManager";
|
||||
import { Species } from "#enums/species";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { getMovePosition } from "../utils/gameManagerUtils";
|
||||
import { BerryPhase, CommandPhase } from "#app/phases.js";
|
||||
import { BattleStat } from "#app/data/battle-stat.js";
|
||||
|
||||
const TIMEOUT = 20 * 1000;
|
||||
|
||||
describe("Moves - Quick Guard", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
|
||||
game.override.battleType("double");
|
||||
|
||||
game.override.moveset([Moves.QUICK_GUARD, Moves.SPLASH, Moves.FOLLOW_ME]);
|
||||
|
||||
game.override.enemySpecies(Species.SNORLAX);
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.QUICK_ATTACK));
|
||||
game.override.enemyAbility(Abilities.INSOMNIA);
|
||||
|
||||
game.override.startingLevel(100);
|
||||
game.override.enemyLevel(100);
|
||||
});
|
||||
|
||||
test(
|
||||
"should protect the user and allies from priority moves",
|
||||
async () => {
|
||||
await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerField();
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.QUICK_GUARD));
|
||||
|
||||
await game.phaseInterceptor.to(CommandPhase);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
leadPokemon.forEach(p => expect(p.hp).toBe(p.getMaxHp()));
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"should protect the user and allies from Prankster-boosted moves",
|
||||
async () => {
|
||||
game.override.enemyAbility(Abilities.PRANKSTER);
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.GROWL));
|
||||
|
||||
await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerField();
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.QUICK_GUARD));
|
||||
|
||||
await game.phaseInterceptor.to(CommandPhase);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
leadPokemon.forEach(p => expect(p.summonData.battleStats[BattleStat.ATK]).toBe(0));
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"should stop subsequent hits of a multi-hit priority move",
|
||||
async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.WATER_SHURIKEN));
|
||||
|
||||
await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerField();
|
||||
const enemyPokemon = game.scene.getEnemyField();
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.QUICK_GUARD));
|
||||
|
||||
await game.phaseInterceptor.to(CommandPhase);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 1, Moves.FOLLOW_ME));
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
leadPokemon.forEach(p => expect(p.hp).toBe(p.getMaxHp()));
|
||||
enemyPokemon.forEach(p => expect(p.turnData.hitCount).toBe(1));
|
||||
}
|
||||
);
|
||||
});
|
125
src/test/moves/wide_guard.test.ts
Normal file
125
src/test/moves/wide_guard.test.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
||||
import GameManager from "../utils/gameManager";
|
||||
import { Species } from "#enums/species";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { getMovePosition } from "../utils/gameManagerUtils";
|
||||
import { BerryPhase, CommandPhase } from "#app/phases.js";
|
||||
import { BattleStat } from "#app/data/battle-stat.js";
|
||||
|
||||
const TIMEOUT = 20 * 1000;
|
||||
|
||||
describe("Moves - Wide Guard", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
|
||||
game.override.battleType("double");
|
||||
|
||||
game.override.moveset([Moves.WIDE_GUARD, Moves.SPLASH, Moves.SURF]);
|
||||
|
||||
game.override.enemySpecies(Species.SNORLAX);
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.SWIFT));
|
||||
game.override.enemyAbility(Abilities.INSOMNIA);
|
||||
|
||||
game.override.startingLevel(100);
|
||||
game.override.enemyLevel(100);
|
||||
});
|
||||
|
||||
test(
|
||||
"should protect the user and allies from multi-target attack moves",
|
||||
async () => {
|
||||
await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerField();
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.WIDE_GUARD));
|
||||
|
||||
await game.phaseInterceptor.to(CommandPhase);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
leadPokemon.forEach(p => expect(p.hp).toBe(p.getMaxHp()));
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"should protect the user and allies from multi-target status moves",
|
||||
async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.GROWL));
|
||||
|
||||
await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerField();
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.WIDE_GUARD));
|
||||
|
||||
await game.phaseInterceptor.to(CommandPhase);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
leadPokemon.forEach(p => expect(p.summonData.battleStats[BattleStat.ATK]).toBe(0));
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"should not protect the user and allies from single-target moves",
|
||||
async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
|
||||
|
||||
await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerField();
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.WIDE_GUARD));
|
||||
|
||||
await game.phaseInterceptor.to(CommandPhase);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
expect(leadPokemon.some(p => p.hp < p.getMaxHp())).toBeTruthy();
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"should protect the user from its ally's multi-target move",
|
||||
async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.SPLASH));
|
||||
|
||||
await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerField();
|
||||
const enemyPokemon = game.scene.getEnemyField();
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.WIDE_GUARD));
|
||||
|
||||
await game.phaseInterceptor.to(CommandPhase);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 1, Moves.SURF));
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
expect(leadPokemon[0].hp).toBe(leadPokemon[0].getMaxHp());
|
||||
enemyPokemon.forEach(p => expect(p.hp).toBeLessThan(p.getMaxHp()));
|
||||
}, TIMEOUT
|
||||
);
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user