Implement Quick Guard and other conditional team protection moves (#1275)
* Implement conditional protection arena tag Affected moves: - Quick Guard - Wide Guard - Mat Block - Crafty Shield - Feint (updated) * Add support for moves that ignore Protect to conditional protection moves * Comments for protect arena tags * ESLint --------- Co-authored-by: Benjamin Odom <bennybroseph@gmail.com>
This commit is contained in:
parent
5c327e347a
commit
4ffff8e1ee
|
@ -1,7 +1,7 @@
|
|||
import { Arena } from "../field/arena";
|
||||
import { Type } from "./type";
|
||||
import * as Utils from "../utils";
|
||||
import { MoveCategory, allMoves } from "./move";
|
||||
import { MoveCategory, allMoves, MoveTarget } from "./move";
|
||||
import { getPokemonMessage } from "../messages";
|
||||
import Pokemon, { HitResult, PokemonMove } from "../field/pokemon";
|
||||
import { MoveEffectPhase, PokemonHealPhase, StatChangePhase} from "../phases";
|
||||
|
@ -11,6 +11,7 @@ import { Moves } from "./enums/moves";
|
|||
import { ArenaTagType } from "./enums/arena-tag-type";
|
||||
import { BlockNonDirectDamageAbAttr, ProtectStatAbAttr, applyAbAttrs } from "./ability";
|
||||
import { BattleStat } from "./battle-stat";
|
||||
import { CommonAnim, CommonBattleAnim } from "./battle-anims";
|
||||
|
||||
export enum ArenaTagSide {
|
||||
BOTH,
|
||||
|
@ -146,6 +147,128 @@ class AuroraVeilTag extends WeakenMoveScreenTag {
|
|||
}
|
||||
}
|
||||
|
||||
type ProtectConditionFunc = (...args: any[]) => boolean;
|
||||
|
||||
/**
|
||||
* Abstract 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 {
|
||||
protected protectConditionFunc: ProtectConditionFunc;
|
||||
|
||||
constructor(tagType: ArenaTagType, sourceMove: Moves, sourceId: integer, side: ArenaTagSide, condition: ProtectConditionFunc) {
|
||||
super(tagType, 1, sourceMove, sourceId, side);
|
||||
|
||||
this.protectConditionFunc = condition;
|
||||
}
|
||||
|
||||
onAdd(arena: Arena): void {
|
||||
arena.scene.queueMessage(`${super.getMoveName()} protected${this.side === ArenaTagSide.PLAYER ? " your" : this.side === ArenaTagSide.ENEMY ? " the\nopposing" : ""} team!`);
|
||||
}
|
||||
|
||||
// Removes default message for effect removal
|
||||
onRemove(arena: Arena): void { }
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @returns
|
||||
*/
|
||||
apply(arena: Arena, args: any[]): boolean {
|
||||
if ((args[0] as Utils.BooleanHolder).value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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(`${super.getMoveName()} protected ${getPokemonMessage(target, "!")}`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* can be an ally or enemy.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onAdd(arena: Arena) {
|
||||
const source = arena.scene.getPokemonById(this.sourceId);
|
||||
arena.scene.queueMessage(getPokemonMessage(source, " intends to flip up a mat\nand block incoming attacks!"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* not target all Pokemon or sides of the field.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class WishTag extends ArenaTag {
|
||||
private battlerIndex: BattlerIndex;
|
||||
private triggerMessage: string;
|
||||
|
@ -513,6 +636,14 @@ export function getArenaTag(tagType: ArenaTagType, turnCount: integer, sourceMov
|
|||
switch (tagType) {
|
||||
case ArenaTagType.MIST:
|
||||
return new MistTag(turnCount, sourceId, side);
|
||||
case ArenaTagType.QUICK_GUARD:
|
||||
return new QuickGuardTag(sourceId, side);
|
||||
case ArenaTagType.WIDE_GUARD:
|
||||
return new WideGuardTag(sourceId, side);
|
||||
case ArenaTagType.MAT_BLOCK:
|
||||
return new MatBlockTag(sourceId, side);
|
||||
case ArenaTagType.CRAFTY_SHIELD:
|
||||
return new CraftyShieldTag(sourceId, side);
|
||||
case ArenaTagType.MUD_SPORT:
|
||||
return new MudSportTag(turnCount, sourceId);
|
||||
case ArenaTagType.WATER_SPORT:
|
||||
|
|
|
@ -16,5 +16,9 @@ export enum ArenaTagType {
|
|||
REFLECT = "REFLECT",
|
||||
LIGHT_SCREEN = "LIGHT_SCREEN",
|
||||
AURORA_VEIL = "AURORA_VEIL",
|
||||
QUICK_GUARD = "QUICK_GUARD",
|
||||
WIDE_GUARD = "WIDE_GUARD",
|
||||
MAT_BLOCK = "MAT_BLOCK",
|
||||
CRAFTY_SHIELD = "CRAFTY_SHIELD",
|
||||
TAILWIND = "TAILWIND"
|
||||
}
|
||||
|
|
|
@ -204,6 +204,19 @@ export default class Move implements Localizable {
|
|||
return false;
|
||||
}
|
||||
|
||||
isAllyTarget(): boolean {
|
||||
switch (this.moveTarget) {
|
||||
case MoveTarget.USER:
|
||||
case MoveTarget.NEAR_ALLY:
|
||||
case MoveTarget.ALLY:
|
||||
case MoveTarget.USER_OR_NEAR_ALLY:
|
||||
case MoveTarget.USER_AND_ALLIES:
|
||||
case MoveTarget.USER_SIDE:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
isTypeImmune(type: Type): boolean {
|
||||
switch (type) {
|
||||
case Type.GRASS:
|
||||
|
@ -3720,6 +3733,37 @@ export class AddArenaTagAttr extends MoveEffectAttr {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic class for removing arena tags
|
||||
* @param tagTypes: The types of tags that can be removed
|
||||
* @param selfSideTarget: Is the user removing tags from its own side?
|
||||
*/
|
||||
export class RemoveArenaTagsAttr extends MoveEffectAttr {
|
||||
public tagTypes: ArenaTagType[];
|
||||
public selfSideTarget: boolean;
|
||||
|
||||
constructor(tagTypes: ArenaTagType[], selfSideTarget: boolean) {
|
||||
super(true, MoveEffectTrigger.POST_APPLY);
|
||||
|
||||
this.tagTypes = tagTypes;
|
||||
this.selfSideTarget = selfSideTarget;
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (!super.apply(user, target, move, args)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const side = (this.selfSideTarget ? user : target).isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||
|
||||
for (const tagType of this.tagTypes) {
|
||||
user.scene.arena.removeTagOnSide(tagType, side);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class AddArenaTrapTagAttr extends AddArenaTagAttr {
|
||||
getCondition(): MoveConditionFunc {
|
||||
return (user, target, move) => {
|
||||
|
@ -5909,6 +5953,7 @@ export function initMoves() {
|
|||
.unimplemented(),
|
||||
new AttackMove(Moves.FEINT, Type.NORMAL, MoveCategory.PHYSICAL, 30, 100, 10, -1, 2, 4)
|
||||
.attr(RemoveBattlerTagAttr, [ BattlerTagType.PROTECTED ])
|
||||
.attr(RemoveArenaTagsAttr, [ ArenaTagType.QUICK_GUARD, ArenaTagType.WIDE_GUARD, ArenaTagType.MAT_BLOCK, ArenaTagType.CRAFTY_SHIELD ], false)
|
||||
.makesContact(false)
|
||||
.ignoresProtect(),
|
||||
new AttackMove(Moves.PLUCK, Type.FLYING, MoveCategory.PHYSICAL, 60, 100, 20, -1, 0, 4)
|
||||
|
@ -6198,7 +6243,7 @@ export function initMoves() {
|
|||
.attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.ACC ], 1, true),
|
||||
new StatusMove(Moves.WIDE_GUARD, Type.ROCK, -1, 10, -1, 3, 5)
|
||||
.target(MoveTarget.USER_SIDE)
|
||||
.unimplemented(),
|
||||
.attr(AddArenaTagAttr, ArenaTagType.WIDE_GUARD, 1, true, true),
|
||||
new StatusMove(Moves.GUARD_SPLIT, Type.PSYCHIC, -1, 10, -1, 0, 5)
|
||||
.unimplemented(),
|
||||
new StatusMove(Moves.POWER_SPLIT, Type.PSYCHIC, -1, 10, -1, 0, 5)
|
||||
|
@ -6286,7 +6331,7 @@ export function initMoves() {
|
|||
.attr(StatChangeCountPowerAttr),
|
||||
new StatusMove(Moves.QUICK_GUARD, Type.FIGHTING, -1, 15, -1, 3, 5)
|
||||
.target(MoveTarget.USER_SIDE)
|
||||
.unimplemented(),
|
||||
.attr(AddArenaTagAttr, ArenaTagType.QUICK_GUARD, 1, true, true),
|
||||
new SelfStatusMove(Moves.ALLY_SWITCH, Type.PSYCHIC, -1, 15, -1, 2, 5)
|
||||
.ignoresProtect()
|
||||
.unimplemented(),
|
||||
|
@ -6452,7 +6497,9 @@ export function initMoves() {
|
|||
.attr(HitsTagAttr, BattlerTagType.MINIMIZED, true)
|
||||
.condition(failOnGravityCondition),
|
||||
new StatusMove(Moves.MAT_BLOCK, Type.FIGHTING, -1, 10, -1, 0, 6)
|
||||
.unimplemented(),
|
||||
.target(MoveTarget.USER_SIDE)
|
||||
.attr(AddArenaTagAttr, ArenaTagType.MAT_BLOCK, 1, true, true)
|
||||
.condition(new FirstMoveCondition()),
|
||||
new AttackMove(Moves.BELCH, Type.POISON, MoveCategory.SPECIAL, 120, 90, 10, -1, 0, 6)
|
||||
.condition((user, target, move) => user.battleData.berriesEaten.length > 0),
|
||||
new StatusMove(Moves.ROTOTILLER, Type.GROUND, -1, 10, 100, 0, 6)
|
||||
|
@ -6505,7 +6552,7 @@ export function initMoves() {
|
|||
.triageMove(),
|
||||
new StatusMove(Moves.CRAFTY_SHIELD, Type.FAIRY, -1, 10, -1, 3, 6)
|
||||
.target(MoveTarget.USER_SIDE)
|
||||
.unimplemented(),
|
||||
.attr(AddArenaTagAttr, ArenaTagType.CRAFTY_SHIELD, 1, true, true),
|
||||
new StatusMove(Moves.FLOWER_SHIELD, Type.FAIRY, -1, 10, 100, 0, 6)
|
||||
.target(MoveTarget.ALL)
|
||||
.unimplemented(),
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Variant, VariantSet, variantColorCache } from "#app/data/variant";
|
|||
import { variantData } from "#app/data/variant";
|
||||
import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "../ui/battle-info";
|
||||
import { Moves } from "../data/enums/moves";
|
||||
import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, VariablePowerAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, MultiHitAttr, StatusMoveTypeImmunityAttr, MoveTarget, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatChangesAttr, SacrificialAttr, VariableMoveTypeAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit } from "../data/move";
|
||||
import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, VariablePowerAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, MultiHitAttr, StatusMoveTypeImmunityAttr, MoveTarget, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatChangesAttr, SacrificialAttr, VariableMoveTypeAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, MoveFlags } from "../data/move";
|
||||
import { default as PokemonSpecies, PokemonSpeciesForm, SpeciesFormKey, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm, getStarterValueFriendshipCap, speciesStarters, starterPassiveAbilities } from "../data/pokemon-species";
|
||||
import * as Utils from "../utils";
|
||||
import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from "../data/type";
|
||||
|
@ -1613,6 +1613,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
typeMultiplier.value = 0;
|
||||
}
|
||||
|
||||
// Apply arena tags for conditional protection
|
||||
if (!move.hasFlag(MoveFlags.IGNORE_PROTECT) && !move.isAllyTarget()) {
|
||||
const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||
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);
|
||||
}
|
||||
|
||||
switch (moveCategory) {
|
||||
case MoveCategory.PHYSICAL:
|
||||
case MoveCategory.SPECIAL:
|
||||
|
|
Loading…
Reference in New Issue