From a6f2e5ba04ffacf917c2b311741674c3bd5a53c5 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sat, 2 Aug 2025 15:07:10 -0400 Subject: [PATCH 01/10] Removed Mortal-spin exclusive `LapseTagAttr` --- src/data/moves/move.ts | 39 ++++++++++----------------------------- 1 file changed, 10 insertions(+), 29 deletions(-) diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index bde5f2977d8..8a376df0129 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -1,3 +1,4 @@ +import type { BattlerTag } from "#data/battler-tags"; import { AbAttrParamsWithCancel, PreAttackModifyPowerAbAttrParams } from "#abilities/ability"; import { applyAbAttrs @@ -5825,45 +5826,25 @@ export class CurseAttr extends MoveEffectAttr { } } -export class LapseBattlerTagAttr extends MoveEffectAttr { - public tagTypes: BattlerTagType[]; - - constructor(tagTypes: BattlerTagType[], selfTarget: boolean = false) { - super(selfTarget); - - this.tagTypes = tagTypes; - } - - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - if (!super.apply(user, target, move, args)) { - return false; - } - - for (const tagType of this.tagTypes) { - (this.selfTarget ? user : target).lapseTag(tagType); - } - - return true; - } -} - +/** + * Attribute to remove all {@linkcode BattlerTag}s matching one or more tag types. + */ export class RemoveBattlerTagAttr extends MoveEffectAttr { - public tagTypes: BattlerTagType[]; +/** An array of {@linkcode BattlerTagType}s to clear. */ + public tagTypes: [BattlerTagType, ...BattlerTagType[]]; - constructor(tagTypes: BattlerTagType[], selfTarget: boolean = false) { + constructor(tagTypes: [BattlerTagType, ...BattlerTagType[]], selfTarget = false) { super(selfTarget); this.tagTypes = tagTypes; } - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - if (!super.apply(user, target, move, args)) { + apply(user: Pokemon, target: Pokemon, move: Move, _args: any[]): boolean { + if (!super.apply(user, target, move, _args)) { return false; } - for (const tagType of this.tagTypes) { - (this.selfTarget ? user : target).removeTag(tagType); - } + (this.selfTarget ? user : target).findAndRemoveTags(t => this.tagTypes.includes(t.tagType)); return true; } From 3a88045871d1072148338e5239585efd30f63a06 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sat, 2 Aug 2025 15:42:58 -0400 Subject: [PATCH 02/10] Removed unused parameter from `addBattlerTagAttr` --- src/data/moves/move.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 8a376df0129..7c99256af77 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -5599,7 +5599,6 @@ export class AddBattlerTagAttr extends MoveEffectAttr { public tagType: BattlerTagType; public turnCountMin: number; public turnCountMax: number; - protected cancelOnFail: boolean; private failOnOverlap: boolean; constructor(tagType: BattlerTagType, selfTarget: boolean = false, failOnOverlap: boolean = false, turnCountMin: number = 0, turnCountMax?: number, lastHitOnly: boolean = false) { From 065659c3969641c7e7c7399aafabc5093b284677 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sat, 2 Aug 2025 16:14:34 -0400 Subject: [PATCH 03/10] Actually removed mortal spin attribute --- src/data/moves/move.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 7c99256af77..21f7572f8e6 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -8409,7 +8409,6 @@ const MoveAttrs = Object.freeze({ GulpMissileTagAttr, JawLockAttr, CurseAttr, - LapseBattlerTagAttr, RemoveBattlerTagAttr, FlinchAttr, ConfuseAttr, @@ -11310,7 +11309,7 @@ export function initMoves() { new AttackMove(MoveId.TRIPLE_DIVE, PokemonType.WATER, MoveCategory.PHYSICAL, 30, 95, 10, -1, 0, 9) .attr(MultiHitAttr, MultiHitType._3), new AttackMove(MoveId.MORTAL_SPIN, PokemonType.POISON, MoveCategory.PHYSICAL, 30, 100, 15, 100, 0, 9) - .attr(LapseBattlerTagAttr, [ + .attr(RemoveBattlerTagAttr, [ BattlerTagType.BIND, BattlerTagType.WRAP, BattlerTagType.FIRE_SPIN, From 2439754aed224e5f2f51d0eed9a550cb9d5b35cf Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sat, 2 Aug 2025 16:17:56 -0400 Subject: [PATCH 04/10] De-janked params of `addBattlerTagAttr` --- src/data/moves/move.ts | 17 +++++++++-------- src/phases/move-effect-phase.ts | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 21f7572f8e6..acc265bc70f 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -5601,13 +5601,13 @@ export class AddBattlerTagAttr extends MoveEffectAttr { public turnCountMax: number; private failOnOverlap: boolean; - constructor(tagType: BattlerTagType, selfTarget: boolean = false, failOnOverlap: boolean = false, turnCountMin: number = 0, turnCountMax?: number, lastHitOnly: boolean = false) { - super(selfTarget, { lastHitOnly: lastHitOnly }); + constructor(tagType: BattlerTagType, selfTarget: boolean = false, failOnOverlap = false, turnCountMin = 0, turnCountMax = turnCountMin, lastHitOnly = false) { + super(selfTarget, { lastHitOnly }); this.tagType = tagType; this.turnCountMin = turnCountMin; - this.turnCountMax = turnCountMax !== undefined ? turnCountMax : turnCountMin; - this.failOnOverlap = !!failOnOverlap; + this.turnCountMax = turnCountMax; + this.failOnOverlap = failOnOverlap; } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { @@ -6013,11 +6013,11 @@ export class AddArenaTagAttr extends MoveEffectAttr { private failOnOverlap: boolean; public selfSideTarget: boolean; - constructor(tagType: ArenaTagType, turnCount?: number | null, failOnOverlap: boolean = false, selfSideTarget: boolean = false) { + constructor(tagType: ArenaTagType, turnCount = 0, failOnOverlap = false, selfSideTarget: boolean = false) { super(true); this.tagType = tagType; - this.turnCount = turnCount!; // TODO: is the bang correct? + this.turnCount = turnCount; this.failOnOverlap = failOnOverlap; this.selfSideTarget = selfSideTarget; } @@ -6027,6 +6027,7 @@ export class AddArenaTagAttr extends MoveEffectAttr { return false; } +// TODO: Why does this check effect chance if nothing uses it? if ((move.chance < 0 || move.chance === 100 || user.randBattleSeedInt(100) < move.chance) && user.getLastXMoves(1)[0]?.result === MoveResult.SUCCESS) { const side = ((this.selfSideTarget ? user : target).isPlayer() !== (move.hasAttr("AddArenaTrapTagAttr") && target === user)) ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; globalScene.arena.addTag(this.tagType, this.turnCount, move.id, user.id, side); @@ -6038,7 +6039,7 @@ export class AddArenaTagAttr extends MoveEffectAttr { getCondition(): MoveConditionFunc | null { return this.failOnOverlap - ? (user, target, move) => !globalScene.arena.getTagOnSide(this.tagType, target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY) + ? (_user, target, _move) => !globalScene.arena.getTagOnSide(this.tagType, target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY) : null; } } @@ -10397,7 +10398,7 @@ export function initMoves() { .target(MoveTarget.USER_AND_ALLIES) .condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ AbilityId.PLUS, AbilityId.MINUS ].find(a => p?.hasAbility(a, false)))), new StatusMove(MoveId.HAPPY_HOUR, PokemonType.NORMAL, -1, 30, -1, 0, 6) // No animation - .attr(AddArenaTagAttr, ArenaTagType.HAPPY_HOUR, null, true) + .attr(AddArenaTagAttr, ArenaTagType.HAPPY_HOUR, 0, true) .target(MoveTarget.USER_SIDE), new StatusMove(MoveId.ELECTRIC_TERRAIN, PokemonType.ELECTRIC, -1, 10, -1, 0, 6) .attr(TerrainChangeAttr, TerrainType.ELECTRIC) diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index c57e0f6cead..99dda6d4bec 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -745,7 +745,7 @@ export class MoveEffectPhase extends PokemonPhase { /** * Applies all move effects that trigger in the event of a successful hit: * - * - {@linkcode MoveEffectTrigger.PRE_APPLY | PRE_APPLY} effects` + * - {@linkcode MoveEffectTrigger.PRE_APPLY | PRE_APPLY} effects * - Applying damage to the target * - {@linkcode MoveEffectTrigger.POST_APPLY | POST_APPLY} effects * - Invoking {@linkcode applyOnTargetEffects} if the move does not hit a substitute From 1b9e4e05eeda2b85d1bc620af5ffbd443c046e6a Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sat, 2 Aug 2025 16:23:12 -0400 Subject: [PATCH 05/10] Significantly cleaned up `RemoveArenaTrapAttr` and `RemoveScreensAttr` --- src/data/moves/move.ts | 153 +++++++++++++++-------------------------- src/field/arena.ts | 34 ++++++++- 2 files changed, 90 insertions(+), 97 deletions(-) diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index acc265bc70f..5ba27c88013 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -1307,15 +1307,13 @@ export class MoveEffectAttr extends MoveAttr { * @param args Set of unique arguments needed by this attribute * @returns true if basic application of the ability attribute should be possible */ - canApply(user: Pokemon, target: Pokemon, move: Move, args?: any[]) { - return !! (this.selfTarget ? user.hp && !user.getTag(BattlerTagType.FRENZY) : target.hp) - && (this.selfTarget || !target.getTag(BattlerTagType.PROTECTED) || - move.doesFlagEffectApply({ flag: MoveFlags.IGNORE_PROTECT, user, target })); + canApply(user: Pokemon, target: Pokemon, move: Move, _args?: any[]) { + return (this.selfTarget ? user : target).hp > 0 } /** Applies move effects so long as they are able based on {@linkcode canApply} */ - apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean { - return this.canApply(user, target, move, args); + apply(user: Pokemon, target: Pokemon, move: Move, _args?: any[]): boolean { + return this.canApply(user, target, move, _args); } /** @@ -5615,9 +5613,10 @@ export class AddBattlerTagAttr extends MoveEffectAttr { return false; } + // TODO: Do any moves actually use chance-based battler tag adding? const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true); if (moveChance < 0 || moveChance === 100 || user.randBattleSeedInt(100) < moveChance) { - return (this.selfTarget ? user : target).addTag(this.tagType, user.randBattleSeedIntRange(this.turnCountMin, this.turnCountMax), move.id, user.id); + return (this.selfTarget ? user : target).addTag(this.tagType, user.randBattleSeedIntRange(this.turnCountMin, this.turnCountMax), move.id, user.id); } return false; @@ -5829,7 +5828,7 @@ export class CurseAttr extends MoveEffectAttr { * Attribute to remove all {@linkcode BattlerTag}s matching one or more tag types. */ export class RemoveBattlerTagAttr extends MoveEffectAttr { -/** An array of {@linkcode BattlerTagType}s to clear. */ + /** An array of {@linkcode BattlerTagType}s to clear. */ public tagTypes: [BattlerTagType, ...BattlerTagType[]]; constructor(tagTypes: [BattlerTagType, ...BattlerTagType[]], selfTarget = false) { @@ -5843,7 +5842,7 @@ export class RemoveBattlerTagAttr extends MoveEffectAttr { return false; } - (this.selfTarget ? user : target).findAndRemoveTags(t => this.tagTypes.includes(t.tagType)); + (this.selfTarget ? user : target).findAndRemoveTags(t => this.tagTypes.includes(t.tagType)); return true; } @@ -5910,6 +5909,8 @@ export class ProtectAttr extends AddBattlerTagAttr { } } +// TODO: Attributes should not exist solely to display a message + export class IgnoreAccuracyAttr extends AddBattlerTagAttr { constructor() { super(BattlerTagType.IGNORE_ACCURACY, true, false, 2); @@ -5981,14 +5982,13 @@ export class RemoveAllSubstitutesAttr extends MoveEffectAttr { export class HitsTagAttr extends MoveAttr { /** The {@linkcode BattlerTagType} this move hits */ public tagType: BattlerTagType; - /** Should this move deal double damage against {@linkcode HitsTagAttr.tagType}? */ + /** Should this move deal double damage against {@linkcode this.tagType}? */ public doubleDamage: boolean; - constructor(tagType: BattlerTagType, doubleDamage: boolean = false) { + constructor(tagType: BattlerTagType) { super(); this.tagType = tagType; - this.doubleDamage = !!doubleDamage; } getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { @@ -6003,7 +6003,8 @@ export class HitsTagAttr extends MoveAttr { */ export class HitsTagForDoubleDamageAttr extends HitsTagAttr { constructor(tagType: BattlerTagType) { - super(tagType, true); + super(tagType); + this.doubleDamage = true; } } @@ -6027,7 +6028,7 @@ export class AddArenaTagAttr extends MoveEffectAttr { return false; } -// TODO: Why does this check effect chance if nothing uses it? + // TODO: Why does this check effect chance if nothing uses it? if ((move.chance < 0 || move.chance === 100 || user.randBattleSeedInt(100) < move.chance) && user.getLastXMoves(1)[0]?.result === MoveResult.SUCCESS) { const side = ((this.selfSideTarget ? user : target).isPlayer() !== (move.hasAttr("AddArenaTrapTagAttr") && target === user)) ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; globalScene.arena.addTag(this.tagType, this.turnCount, move.id, user.id, side); @@ -6045,19 +6046,22 @@ 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? + * Attribute to remove one or more arena tags from the field. */ export class RemoveArenaTagsAttr extends MoveEffectAttr { - public tagTypes: ArenaTagType[]; - public selfSideTarget: boolean; + /** An array containing the tags to be removed. */ + private readonly tagTypes: readonly [ArenaTagType, ...ArenaTagType[]]; + /** + * Whether to remove tags from both sides of the field (`true`) or + * the target's side of the field (`false`); default `false` + */ + private removeAllTags: boolean - constructor(tagTypes: ArenaTagType[], selfSideTarget: boolean) { - super(true); + constructor(tagTypes: readonly [ArenaTagType, ...ArenaTagType[]], removeAllTags = false, options?: MoveEffectAttrOptions) { + super(true, options); this.tagTypes = tagTypes; - this.selfSideTarget = selfSideTarget; + this.removeAllTags = removeAllTags; } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { @@ -6065,11 +6069,9 @@ export class RemoveArenaTagsAttr extends MoveEffectAttr { return false; } - const side = (this.selfSideTarget ? user : target).isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; + const side = this.removeAllTags ? ArenaTagSide.BOTH : target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; - for (const tagType of this.tagTypes) { - globalScene.arena.removeTagOnSide(tagType, side); - } + globalScene.arena.removeTagsOnSide(this.tagTypes, side); return true; } @@ -6115,80 +6117,37 @@ export class AddArenaTrapTagHitAttr extends AddArenaTagAttr { } } -export class RemoveArenaTrapAttr extends MoveEffectAttr { +// TODO: Review if we can remove these attributes +const arenaTrapTags = [ + ArenaTagType.SPIKES, + ArenaTagType.TOXIC_SPIKES, + ArenaTagType.STEALTH_ROCK, + ArenaTagType.STICKY_WEB, +] as const; - private targetBothSides: boolean; - - constructor(targetBothSides: boolean = false) { - super(true, { trigger: MoveEffectTrigger.PRE_APPLY }); - this.targetBothSides = targetBothSides; - } - - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - - if (!super.apply(user, target, move, args)) { - return false; - } - - if (this.targetBothSides) { - globalScene.arena.removeTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.PLAYER); - globalScene.arena.removeTagOnSide(ArenaTagType.TOXIC_SPIKES, ArenaTagSide.PLAYER); - globalScene.arena.removeTagOnSide(ArenaTagType.STEALTH_ROCK, ArenaTagSide.PLAYER); - globalScene.arena.removeTagOnSide(ArenaTagType.STICKY_WEB, ArenaTagSide.PLAYER); - - globalScene.arena.removeTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY); - globalScene.arena.removeTagOnSide(ArenaTagType.TOXIC_SPIKES, ArenaTagSide.ENEMY); - globalScene.arena.removeTagOnSide(ArenaTagType.STEALTH_ROCK, ArenaTagSide.ENEMY); - globalScene.arena.removeTagOnSide(ArenaTagType.STICKY_WEB, ArenaTagSide.ENEMY); - } else { - globalScene.arena.removeTagOnSide(ArenaTagType.SPIKES, target.isPlayer() ? ArenaTagSide.ENEMY : ArenaTagSide.PLAYER); - globalScene.arena.removeTagOnSide(ArenaTagType.TOXIC_SPIKES, target.isPlayer() ? ArenaTagSide.ENEMY : ArenaTagSide.PLAYER); - globalScene.arena.removeTagOnSide(ArenaTagType.STEALTH_ROCK, target.isPlayer() ? ArenaTagSide.ENEMY : ArenaTagSide.PLAYER); - globalScene.arena.removeTagOnSide(ArenaTagType.STICKY_WEB, target.isPlayer() ? ArenaTagSide.ENEMY : ArenaTagSide.PLAYER); - } - - return true; +export class RemoveArenaTrapAttr extends RemoveArenaTagsAttr { + constructor(targetBothSides = false) { + // TODO: This triggers at a different time than {@linkcode RemoveArenaTagsAbAttr}... + super(arenaTrapTags, targetBothSides, { trigger: MoveEffectTrigger.PRE_APPLY }); } } -export class RemoveScreensAttr extends MoveEffectAttr { - - private targetBothSides: boolean; - - constructor(targetBothSides: boolean = false) { - super(true, { trigger: MoveEffectTrigger.PRE_APPLY }); - this.targetBothSides = targetBothSides; - } - - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - - if (!super.apply(user, target, move, args)) { - return false; - } - - if (this.targetBothSides) { - globalScene.arena.removeTagOnSide(ArenaTagType.REFLECT, ArenaTagSide.PLAYER); - globalScene.arena.removeTagOnSide(ArenaTagType.LIGHT_SCREEN, ArenaTagSide.PLAYER); - globalScene.arena.removeTagOnSide(ArenaTagType.AURORA_VEIL, ArenaTagSide.PLAYER); - - globalScene.arena.removeTagOnSide(ArenaTagType.REFLECT, ArenaTagSide.ENEMY); - globalScene.arena.removeTagOnSide(ArenaTagType.LIGHT_SCREEN, ArenaTagSide.ENEMY); - globalScene.arena.removeTagOnSide(ArenaTagType.AURORA_VEIL, ArenaTagSide.ENEMY); - } else { - globalScene.arena.removeTagOnSide(ArenaTagType.REFLECT, target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY); - globalScene.arena.removeTagOnSide(ArenaTagType.LIGHT_SCREEN, target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY); - globalScene.arena.removeTagOnSide(ArenaTagType.AURORA_VEIL, target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY); - } - - return true; +const screenTags = [ + ArenaTagType.REFLECT, + ArenaTagType.LIGHT_SCREEN, + ArenaTagType.AURORA_VEIL +] as const; +export class RemoveScreensAttr extends RemoveArenaTagsAttr { + constructor(targetBothSides = false) { + // TODO: This triggers at a different time than {@linkcode RemoveArenaTagsAbAttr}... + super(arenaTrapTags, targetBothSides, { trigger: MoveEffectTrigger.PRE_APPLY }); } } -/*Swaps arena effects between the player and enemy side - * @extends MoveEffectAttr - * @see {@linkcode apply} -*/ +/** + * Swaps arena effects between the player and enemy side + */ export class SwapArenaTagsAttr extends MoveEffectAttr { public SwapTags: ArenaTagType[]; @@ -6583,6 +6542,7 @@ export class ChillyReceptionAttr extends ForceSwitchOutAttr { return (user, target, move) => globalScene.arena.weather?.weatherType !== WeatherType.SNOW || super.getSwitchOutCondition()(user, target, move); } } + export class RemoveTypeAttr extends MoveEffectAttr { private removedType: PokemonType; @@ -8973,7 +8933,7 @@ export function initMoves() { .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1) .reflectable(), new StatusMove(MoveId.MIND_READER, PokemonType.NORMAL, -1, 5, -1, 0, 2) - .attr(IgnoreAccuracyAttr), + .attr(AddBattlerTagAttr, BattlerTagType.IGNORE_ACCURACY, true, false, 2), new StatusMove(MoveId.NIGHTMARE, PokemonType.GHOST, 100, 15, -1, 0, 2) .attr(AddBattlerTagAttr, BattlerTagType.NIGHTMARE) .condition(targetSleptOrComatoseCondition), @@ -9077,7 +9037,7 @@ export function initMoves() { .attr(MultiHitAttr) .makesContact(false), new StatusMove(MoveId.LOCK_ON, PokemonType.NORMAL, -1, 5, -1, 0, 2) - .attr(IgnoreAccuracyAttr), + .attr(AddBattlerTagAttr, BattlerTagType.IGNORE_ACCURACY, true, false, 2), new AttackMove(MoveId.OUTRAGE, PokemonType.DRAGON, MoveCategory.PHYSICAL, 120, 100, 10, -1, 0, 2) .attr(FrenzyAttr) .attr(MissEffectAttr, frenzyMissFunc) @@ -10581,7 +10541,7 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.SPD ], -1) .reflectable(), new SelfStatusMove(MoveId.LASER_FOCUS, PokemonType.NORMAL, -1, 30, -1, 0, 7) - .attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false), + .attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false, 2), new StatusMove(MoveId.GEAR_UP, PokemonType.STEEL, -1, 20, -1, 0, 7) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => !![ AbilityId.PLUS, AbilityId.MINUS ].find(a => target.hasAbility(a, false)) }) .ignoresSubstitute() @@ -10670,7 +10630,7 @@ export function initMoves() { new AttackMove(MoveId.MALICIOUS_MOONSAULT, PokemonType.DARK, MoveCategory.PHYSICAL, 180, -1, 1, -1, 0, 7) .unimplemented() .attr(AlwaysHitMinimizeAttr) - .attr(HitsTagAttr, BattlerTagType.MINIMIZED, true) + .attr(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED) .edgeCase(), // I assume it's because it needs darkest lariat and incineroar new AttackMove(MoveId.OCEANIC_OPERETTA, PokemonType.WATER, MoveCategory.SPECIAL, 195, -1, 1, -1, 0, 7) .unimplemented() @@ -11298,6 +11258,7 @@ export function initMoves() { .attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_GET_HIT, true, false, 0, 0, true) .attr(AddBattlerTagAttr, BattlerTagType.RECEIVE_DOUBLE_DAMAGE, true, false, 0, 0, true) .condition((user, target, move) => { + // TODO: This is really really really janky return !(target.getTag(BattlerTagType.PROTECTED)?.tagType === "PROTECTED" || globalScene.arena.getTag(ArenaTagType.MAT_BLOCK)?.tagType === "MAT_BLOCK"); }), new StatusMove(MoveId.REVIVAL_BLESSING, PokemonType.NORMAL, -1, 1, -1, 0, 9) diff --git a/src/field/arena.ts b/src/field/arena.ts index 484450cc5df..f7a516c77ce 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -718,7 +718,7 @@ export class Arena { } // creates a new tag object - const newTag = getArenaTag(tagType, turnCount || 0, sourceMove, sourceId, side); + const newTag = getArenaTag(tagType, turnCount, sourceMove, sourceId, side); if (newTag) { newTag.onAdd(this, quiet); this.tags.push(newTag); @@ -834,6 +834,38 @@ export class Arena { return !!tag; } + /** + * Find and remove all {@linkcode ArenaTag}s with the given tag types on the given side of the field. + * @param tagTypes - The {@linkcode ArenaTagType}s to remove + * @param side - The {@linkcode ArenaTagSide} to remove the tags from (for side-based tags), or {@linkcode ArenaTagSide.BOTH} + * to clear all tags on either side of the field + * @param quiet - Whether to suppress removal messages from currently-present tags; default `false` + * @todo Review the other tag manipulation functions to see if they can be migrated towards using this (more efficient) + */ + public removeTagsOnSide( + tagTypes: ArenaTagType[] | ReadonlyArray, + side: ArenaTagSide, + quiet = false, + ): void { + const indicesToRemove: number[] = []; + for (const [i, tag] of this.tags.entries()) { + // Skip tags of different types or on the wrong side of the field + if (!tagTypes.includes(tag.tagType)) { + continue; + } + if (!(side === ArenaTagSide.BOTH || tag.side === ArenaTagSide.BOTH || tag.side === side)) { + continue; + } + + indicesToRemove.push(i); + } + + for (const index of indicesToRemove) { + this.tags[index].onRemove(this, quiet); + this.tags.splice(index, 1); + } + } + removeAllTags(): void { while (this.tags.length) { this.tags[0].onRemove(this); From 808e29f55eb741c224e4c689aa4f77f1f71a1330 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sat, 2 Aug 2025 18:40:28 -0400 Subject: [PATCH 06/10] Added `toHaveArenaTagMatcher` --- src/field/arena.ts | 20 +++--- test/@types/vitest.d.ts | 12 ++++ test/moves/tidy-up.test.ts | 62 +++++-------------- test/test-utils/matchers/to-have-arena-tag.ts | 54 ++++++++++++++++ 4 files changed, 91 insertions(+), 57 deletions(-) create mode 100644 test/test-utils/matchers/to-have-arena-tag.ts diff --git a/src/field/arena.ts b/src/field/arena.ts index f7a516c77ce..2bdc01193fb 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -847,23 +847,21 @@ export class Arena { side: ArenaTagSide, quiet = false, ): void { - const indicesToRemove: number[] = []; - for (const [i, tag] of this.tags.entries()) { + const leftoverTags: ArenaTag[] = []; + for (const tag of this.tags) { // Skip tags of different types or on the wrong side of the field - if (!tagTypes.includes(tag.tagType)) { - continue; - } - if (!(side === ArenaTagSide.BOTH || tag.side === ArenaTagSide.BOTH || tag.side === side)) { + if ( + !tagTypes.includes(tag.tagType) || + !(side === ArenaTagSide.BOTH || tag.side === ArenaTagSide.BOTH || tag.side === side) + ) { + leftoverTags.push(tag); continue; } - indicesToRemove.push(i); + tag.onRemove(this, quiet); } - for (const index of indicesToRemove) { - this.tags[index].onRemove(this, quiet); - this.tags.splice(index, 1); - } + this.tags = leftoverTags; } removeAllTags(): void { diff --git a/test/@types/vitest.d.ts b/test/@types/vitest.d.ts index 58b36580727..7e925445119 100644 --- a/test/@types/vitest.d.ts +++ b/test/@types/vitest.d.ts @@ -1,7 +1,11 @@ import type { Pokemon } from "#field/pokemon"; import type { PokemonType } from "#enums/pokemon-type"; import type { expect } from "vitest"; +import type { Arena } from "#field/arena"; +import type { ArenaTag } from "#data/arena-tag"; import type { toHaveTypesOptions } from "#test/test-utils/matchers/to-have-types"; +import { ArenaTagType } from "#enums/arena-tag-type"; +import { toHaveArenaTagOptions } from "#test/test-utils/matchers/to-have-arena-tag"; declare module "vitest" { interface Assertion { @@ -22,5 +26,13 @@ declare module "vitest" { * @param options - The options passed to the matcher. */ toHaveTypes(expected: PokemonType[], options?: toHaveTypesOptions): void; + /** + * Matcher to check if the current {@linkcode Arena} contains the given {@linkcode ArenaTag}. + * + * @param expected - The expected {@linkcode ArenaTagType} + * @param options - The options passed to the matcher + */ + toHaveArenaTag(expected: T, options?: toHaveArenaTagOptions): void; + } } \ No newline at end of file diff --git a/test/moves/tidy-up.test.ts b/test/moves/tidy-up.test.ts index 8dd74e4ab78..05445ca132b 100644 --- a/test/moves/tidy-up.test.ts +++ b/test/moves/tidy-up.test.ts @@ -1,5 +1,6 @@ import { SubstituteTag } from "#data/battler-tags"; import { AbilityId } from "#enums/ability-id"; +import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagType } from "#enums/arena-tag-type"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; @@ -7,6 +8,7 @@ import { Stat } from "#enums/stat"; import { MoveEndPhase } from "#phases/move-end-phase"; import { TurnEndPhase } from "#phases/turn-end-phase"; import { GameManager } from "#test/test-utils/game-manager"; +import type { ArenaTrapTagType } from "#types/arena-tags"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -31,56 +33,24 @@ describe("Moves - Tidy Up", () => { .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH) - .starterSpecies(SpeciesId.FEEBAS) - .ability(AbilityId.BALL_FETCH) - .moveset([MoveId.TIDY_UP]) - .startingLevel(50); + .ability(AbilityId.BALL_FETCH); }); - it("spikes are cleared", async () => { - game.override.moveset([MoveId.SPIKES, MoveId.TIDY_UP]).enemyMoveset(MoveId.SPIKES); - await game.classicMode.startBattle(); + it.each<{ name: string; hazard: ArenaTrapTagType }>([{ name: "Spikes", hazard: ArenaTagType.SPIKES }])( + "should remove $name from both sides of the field", + async ({ hazard }) => { + await game.classicMode.startBattle([SpeciesId.FEEBAS]); - game.move.select(MoveId.SPIKES); - await game.phaseInterceptor.to(TurnEndPhase); - game.move.select(MoveId.TIDY_UP); - await game.phaseInterceptor.to(MoveEndPhase); - expect(game.scene.arena.getTag(ArenaTagType.SPIKES)).toBeUndefined(); - }); + // Add tag to both sides of the field + game.scene.arena.addTag(hazard, 1, undefined, game.field.getPlayerPokemon().id, ArenaTagSide.PLAYER); + game.scene.arena.addTag(hazard, 1, undefined, game.field.getPlayerPokemon().id, ArenaTagSide.ENEMY); - it("stealth rocks are cleared", async () => { - game.override.moveset([MoveId.STEALTH_ROCK, MoveId.TIDY_UP]).enemyMoveset(MoveId.STEALTH_ROCK); - await game.classicMode.startBattle(); - - game.move.select(MoveId.STEALTH_ROCK); - await game.phaseInterceptor.to(TurnEndPhase); - game.move.select(MoveId.TIDY_UP); - await game.phaseInterceptor.to(MoveEndPhase); - expect(game.scene.arena.getTag(ArenaTagType.STEALTH_ROCK)).toBeUndefined(); - }); - - it("toxic spikes are cleared", async () => { - game.override.moveset([MoveId.TOXIC_SPIKES, MoveId.TIDY_UP]).enemyMoveset(MoveId.TOXIC_SPIKES); - await game.classicMode.startBattle(); - - game.move.select(MoveId.TOXIC_SPIKES); - await game.phaseInterceptor.to(TurnEndPhase); - game.move.select(MoveId.TIDY_UP); - await game.phaseInterceptor.to(MoveEndPhase); - expect(game.scene.arena.getTag(ArenaTagType.TOXIC_SPIKES)).toBeUndefined(); - }); - - it("sticky webs are cleared", async () => { - game.override.moveset([MoveId.STICKY_WEB, MoveId.TIDY_UP]).enemyMoveset(MoveId.STICKY_WEB); - - await game.classicMode.startBattle(); - - game.move.select(MoveId.STICKY_WEB); - await game.phaseInterceptor.to(TurnEndPhase); - game.move.select(MoveId.TIDY_UP); - await game.phaseInterceptor.to(MoveEndPhase); - expect(game.scene.arena.getTag(ArenaTagType.STICKY_WEB)).toBeUndefined(); - }); + expect(game.scene.arena.getTag()); + game.move.use(MoveId.TIDY_UP); + await game.toEndOfTurn(); + expect(game.scene.arena.getTag(ArenaTagType.SPIKES)).toBeUndefined(); + }, + ); it("substitutes are cleared", async () => { game.override.moveset([MoveId.SUBSTITUTE, MoveId.TIDY_UP]).enemyMoveset(MoveId.SUBSTITUTE); diff --git a/test/test-utils/matchers/to-have-arena-tag.ts b/test/test-utils/matchers/to-have-arena-tag.ts new file mode 100644 index 00000000000..cc2155ea3b1 --- /dev/null +++ b/test/test-utils/matchers/to-have-arena-tag.ts @@ -0,0 +1,54 @@ +import type { ArenaTagSide } from "#enums/arena-tag-side"; +import { ArenaTagType } from "#enums/arena-tag-type"; +// biome-ignore lint/correctness/noUnusedImports: TSDoc +import type { GameManager } from "#test/test-utils/game-manager"; +import { getEnumStr, stringifyEnumArray } from "#test/test-utils/string-utils"; +import { isGameManagerInstance, receivedStr } from "#test/test-utils/test-utils"; +import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; + +/** + * Matcher to check if the {@linkcode Arena} has a given {@linkcode ArenaTag} active. + * @param received - The object to check. Should be the current {@linkcode GameManager}. + * @param expectedType - The {@linkcode ArenaTagType} of the desired tag + * @param side - The {@linkcode ArenaTagSide | side of the field} the tag should affect, or + * {@linkcode ArenaTagSide.BOTH} to check both sides. + * @param options - The options passed to this matcher + * @returns The result of the matching + */ +export function toHaveArenaTag( + this: MatcherState, + received: unknown, + expectedType: ArenaTagType, + side: ArenaTagSide, +): SyncExpectationResult { + if (!isGameManagerInstance(received)) { + return { + pass: false, + message: () => `Expected to recieve a GameManager, but got ${receivedStr(received)}!`, + }; + } + + if (!received.scene?.arena) { + return { + pass: false, + message: () => `Expected GameManager.${received.scene ? "scene" : "scene.arena"} to be defined!`, + }; + } + + const tag = received.scene.arena.getTagOnSide(expectedType, side); + const pass = !!tag; + const expectedStr = getEnumStr(ArenaTagType, expectedType); + return { + pass, + message: () => + pass + ? `Expected the arena to NOT have a tag matching ${expectedStr}, but it did!` + : // Replace newlines with spaces to preserve one-line ness + `Expected the arena to have a tag matching ${expectedStr}, but it didn't!`, + expected: getEnumStr(ArenaTagType, expectedType), + actual: stringifyEnumArray( + ArenaTagType, + received.scene.arena.tags.map(t => t.tagType), + ), + }; +} From 4dda982ff68937247d01e51cf76d94d745f14cb0 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sun, 3 Aug 2025 13:24:53 -0400 Subject: [PATCH 07/10] Finished up fixing up tidy up tests; removed matcher to go to its own PR --- src/data/moves/move.ts | 1 + test/@types/vitest.d.ts | 8 -- test/moves/tidy-up.test.ts | 84 +++++++++++-------- test/test-utils/matchers/to-have-arena-tag.ts | 54 ------------ 4 files changed, 48 insertions(+), 99 deletions(-) delete mode 100644 test/test-utils/matchers/to-have-arena-tag.ts diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 5ba27c88013..24b263a2a24 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -6096,6 +6096,7 @@ export class AddArenaTrapTagAttr extends AddArenaTagAttr { * @extends AddArenaTagAttr * @see {@linkcode apply} */ +// TODO: This has exactly 1 line of code difference from the base attribute wrt. effect chances... export class AddArenaTrapTagHitAttr extends AddArenaTagAttr { /** * @param user {@linkcode Pokemon} using this move diff --git a/test/@types/vitest.d.ts b/test/@types/vitest.d.ts index ec4d39d7778..a692da5bd65 100644 --- a/test/@types/vitest.d.ts +++ b/test/@types/vitest.d.ts @@ -86,14 +86,6 @@ declare module "vitest" { */ toHaveTerrain(expectedTerrainType: TerrainType): void; - /** - * Matcher to check if the current {@linkcode Arena} contains the given {@linkcode ArenaTag}. - * - * @param expected - The expected {@linkcode ArenaTagType} - * @param options - The options passed to the matcher - */ - toHaveArenaTag(expected: T, options?: toHaveArenaTagOptions): void;v - /** * Check whether a {@linkcode Pokemon} is at full HP. */ diff --git a/test/moves/tidy-up.test.ts b/test/moves/tidy-up.test.ts index 05445ca132b..56c0f338565 100644 --- a/test/moves/tidy-up.test.ts +++ b/test/moves/tidy-up.test.ts @@ -1,12 +1,11 @@ -import { SubstituteTag } from "#data/battler-tags"; import { AbilityId } from "#enums/ability-id"; import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagType } from "#enums/arena-tag-type"; +import { BattlerIndex } from "#enums/battler-index"; +import { BattlerTagType } from "#enums/battler-tag-type"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; -import { MoveEndPhase } from "#phases/move-end-phase"; -import { TurnEndPhase } from "#phases/turn-end-phase"; import { GameManager } from "#test/test-utils/game-manager"; import type { ArenaTrapTagType } from "#types/arena-tags"; import Phaser from "phaser"; @@ -36,51 +35,62 @@ describe("Moves - Tidy Up", () => { .ability(AbilityId.BALL_FETCH); }); - it.each<{ name: string; hazard: ArenaTrapTagType }>([{ name: "Spikes", hazard: ArenaTagType.SPIKES }])( - "should remove $name from both sides of the field", - async ({ hazard }) => { - await game.classicMode.startBattle([SpeciesId.FEEBAS]); + it.each<{ name: string; tagType: ArenaTrapTagType }>([ + { name: "Spikes", tagType: ArenaTagType.SPIKES }, + { name: "Toxic Spikes", tagType: ArenaTagType.TOXIC_SPIKES }, + { name: "Stealth Rock", tagType: ArenaTagType.STEALTH_ROCK }, + { name: "Sticky Web", tagType: ArenaTagType.STICKY_WEB }, + ])("should remove $name from both sides of the field", async ({ tagType }) => { + await game.classicMode.startBattle([SpeciesId.FEEBAS]); - // Add tag to both sides of the field - game.scene.arena.addTag(hazard, 1, undefined, game.field.getPlayerPokemon().id, ArenaTagSide.PLAYER); - game.scene.arena.addTag(hazard, 1, undefined, game.field.getPlayerPokemon().id, ArenaTagSide.ENEMY); + // Add tag to both sides of the field + game.scene.arena.addTag(tagType, 1, undefined, game.field.getPlayerPokemon().id, ArenaTagSide.PLAYER); + game.scene.arena.addTag(tagType, 1, undefined, game.field.getPlayerPokemon().id, ArenaTagSide.ENEMY); - expect(game.scene.arena.getTag()); - game.move.use(MoveId.TIDY_UP); - await game.toEndOfTurn(); - expect(game.scene.arena.getTag(ArenaTagType.SPIKES)).toBeUndefined(); - }, - ); + expect(game.scene.arena.getTag(tagType)).toBeDefined(); - it("substitutes are cleared", async () => { - game.override.moveset([MoveId.SUBSTITUTE, MoveId.TIDY_UP]).enemyMoveset(MoveId.SUBSTITUTE); + game.move.use(MoveId.TIDY_UP); + await game.toEndOfTurn(); - await game.classicMode.startBattle(); + expect(game.scene.arena.getTag(tagType)).toBeUndefined(); + }); - game.move.select(MoveId.SUBSTITUTE); - await game.phaseInterceptor.to(TurnEndPhase); - game.move.select(MoveId.TIDY_UP); - await game.phaseInterceptor.to(MoveEndPhase); + it("should clear substitutes from all pokemon", async () => { + game.override.battleStyle("double"); + await game.classicMode.startBattle([SpeciesId.CINCCINO, SpeciesId.FEEBAS]); - const pokemon = [game.scene.getPlayerPokemon()!, game.scene.getEnemyPokemon()!]; - pokemon.forEach(p => { - expect(p).toBeDefined(); - expect(p!.getTag(SubstituteTag)).toBeUndefined(); + game.move.use(MoveId.SUBSTITUTE, BattlerIndex.PLAYER); + game.move.use(MoveId.SUBSTITUTE, BattlerIndex.PLAYER_2); + await game.move.forceEnemyMove(MoveId.SUBSTITUTE); + await game.move.forceEnemyMove(MoveId.SUBSTITUTE); + await game.toNextTurn(); + + game.scene.getField(true).forEach(p => { + expect(p).toHaveBattlerTag(BattlerTagType.SUBSTITUTE); + }); + + game.move.use(MoveId.TIDY_UP, BattlerIndex.PLAYER); + game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2); + await game.move.forceEnemyMove(MoveId.SPLASH); + await game.move.forceEnemyMove(MoveId.SPLASH); + await game.toEndOfTurn(); + + game.scene.getField(true).forEach(p => { + expect(p).not.toHaveBattlerTag(BattlerTagType.SUBSTITUTE); }); }); - it("user's stats are raised with no traps set", async () => { - await game.classicMode.startBattle(); + it("should raise the user's stats even if a tag cannot be removed", async () => { + await game.classicMode.startBattle([SpeciesId.FEEBAS]); - const playerPokemon = game.scene.getPlayerPokemon()!; + const feebas = game.field.getPlayerPokemon(); + expect(feebas).toHaveStatStage(Stat.ATK, 0); + expect(feebas).toHaveStatStage(Stat.SPD, 0); - expect(playerPokemon.getStatStage(Stat.ATK)).toBe(0); - expect(playerPokemon.getStatStage(Stat.SPD)).toBe(0); + game.move.use(MoveId.TIDY_UP); + await game.toEndOfTurn(); - game.move.select(MoveId.TIDY_UP); - await game.phaseInterceptor.to(TurnEndPhase); - - expect(playerPokemon.getStatStage(Stat.ATK)).toBe(1); - expect(playerPokemon.getStatStage(Stat.SPD)).toBe(1); + expect(feebas).toHaveStatStage(Stat.ATK, 1); + expect(feebas).toHaveStatStage(Stat.SPD, 1); }); }); diff --git a/test/test-utils/matchers/to-have-arena-tag.ts b/test/test-utils/matchers/to-have-arena-tag.ts deleted file mode 100644 index cc2155ea3b1..00000000000 --- a/test/test-utils/matchers/to-have-arena-tag.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { ArenaTagSide } from "#enums/arena-tag-side"; -import { ArenaTagType } from "#enums/arena-tag-type"; -// biome-ignore lint/correctness/noUnusedImports: TSDoc -import type { GameManager } from "#test/test-utils/game-manager"; -import { getEnumStr, stringifyEnumArray } from "#test/test-utils/string-utils"; -import { isGameManagerInstance, receivedStr } from "#test/test-utils/test-utils"; -import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; - -/** - * Matcher to check if the {@linkcode Arena} has a given {@linkcode ArenaTag} active. - * @param received - The object to check. Should be the current {@linkcode GameManager}. - * @param expectedType - The {@linkcode ArenaTagType} of the desired tag - * @param side - The {@linkcode ArenaTagSide | side of the field} the tag should affect, or - * {@linkcode ArenaTagSide.BOTH} to check both sides. - * @param options - The options passed to this matcher - * @returns The result of the matching - */ -export function toHaveArenaTag( - this: MatcherState, - received: unknown, - expectedType: ArenaTagType, - side: ArenaTagSide, -): SyncExpectationResult { - if (!isGameManagerInstance(received)) { - return { - pass: false, - message: () => `Expected to recieve a GameManager, but got ${receivedStr(received)}!`, - }; - } - - if (!received.scene?.arena) { - return { - pass: false, - message: () => `Expected GameManager.${received.scene ? "scene" : "scene.arena"} to be defined!`, - }; - } - - const tag = received.scene.arena.getTagOnSide(expectedType, side); - const pass = !!tag; - const expectedStr = getEnumStr(ArenaTagType, expectedType); - return { - pass, - message: () => - pass - ? `Expected the arena to NOT have a tag matching ${expectedStr}, but it did!` - : // Replace newlines with spaces to preserve one-line ness - `Expected the arena to have a tag matching ${expectedStr}, but it didn't!`, - expected: getEnumStr(ArenaTagType, expectedType), - actual: stringifyEnumArray( - ArenaTagType, - received.scene.arena.tags.map(t => t.tagType), - ), - }; -} From 65f28b7dc4361730b231816d77372315d1f8301e Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sun, 3 Aug 2025 17:54:26 -0400 Subject: [PATCH 08/10] Reverted file --- test/@types/vitest.d.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/test/@types/vitest.d.ts b/test/@types/vitest.d.ts index a692da5bd65..7b756c45a57 100644 --- a/test/@types/vitest.d.ts +++ b/test/@types/vitest.d.ts @@ -3,12 +3,6 @@ import type { AbilityId } from "#enums/ability-id"; import type { BattlerTagType } from "#enums/battler-tag-type"; import type { MoveId } from "#enums/move-id"; import type { PokemonType } from "#enums/pokemon-type"; -import type { expect } from "vitest"; -import type { Arena } from "#field/arena"; -import type { ArenaTag } from "#data/arena-tag"; -import type { toHaveTypesOptions } from "#test/test-utils/matchers/to-have-types"; -import { ArenaTagType } from "#enums/arena-tag-type"; -import { toHaveArenaTagOptions } from "#test/test-utils/matchers/to-have-arena-tag"; import type { BattleStat, EffectiveStat, Stat } from "#enums/stat"; import type { StatusEffect } from "#enums/status-effect"; import type { WeatherType } from "#enums/weather-type"; @@ -41,7 +35,6 @@ declare module "vitest" { * @param expected - The expected types (in any order) * @param options - The options passed to the matcher */ - toHaveTypes(expected: PokemonType[], options?: toHaveTypesOptions): void; toHaveTypes(expected: [PokemonType, ...PokemonType[]], options?: toHaveTypesOptions): void; /** From 6f3d1b911607668f71fe06aed02f3ddaf3c2f85a Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Sun, 3 Aug 2025 20:35:35 -0400 Subject: [PATCH 09/10] Update move.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/moves/move.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 24b263a2a24..5c6b361c533 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -1312,8 +1312,8 @@ export class MoveEffectAttr extends MoveAttr { } /** Applies move effects so long as they are able based on {@linkcode canApply} */ - apply(user: Pokemon, target: Pokemon, move: Move, _args?: any[]): boolean { - return this.canApply(user, target, move, _args); + apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean { + return this.canApply(user, target, move, args); } /** From 1b2cb1de00234a728a278b21ba5a6469927c9bdb Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Sun, 3 Aug 2025 20:35:46 -0400 Subject: [PATCH 10/10] Update move.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/moves/move.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 5c6b361c533..9dacd392c6c 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -1308,7 +1308,7 @@ export class MoveEffectAttr extends MoveAttr { * @returns true if basic application of the ability attribute should be possible */ canApply(user: Pokemon, target: Pokemon, move: Move, _args?: any[]) { - return (this.selfTarget ? user : target).hp > 0 + return !(this.selfTarget ? user : target).isFainted(); } /** Applies move effects so long as they are able based on {@linkcode canApply} */