From 4ffff8e1ee72afba6db0a42368602661acc0b4de Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Sat, 25 May 2024 04:22:10 -0700 Subject: [PATCH] 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 --- src/data/arena-tag.ts | 133 ++++++++++++++++++++++++++++++- src/data/enums/arena-tag-type.ts | 4 + src/data/move.ts | 55 ++++++++++++- src/field/pokemon.ts | 11 ++- 4 files changed, 197 insertions(+), 6 deletions(-) diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index ed1e86b7a8e..3057132cafc 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -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: diff --git a/src/data/enums/arena-tag-type.ts b/src/data/enums/arena-tag-type.ts index 2ecac8b5677..90f45f481ba 100644 --- a/src/data/enums/arena-tag-type.ts +++ b/src/data/enums/arena-tag-type.ts @@ -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" } diff --git a/src/data/move.ts b/src/data/move.ts index 757b5ae148f..6aac106247f 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -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(), diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index c8ba9c86319..4282d7cda13 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -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: