From 6e26db27b864b135303c08b23c88037b9b107f21 Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Thu, 5 Sep 2024 12:07:04 -0700 Subject: [PATCH] Protection moves now fail when used last (#4045) --- src/data/move.ts | 42 ++++++++++++++------- src/test/moves/protect.test.ts | 41 ++++++++++++++++----- src/test/moves/quick_guard.test.ts | 59 ++++++++++++++++++------------ 3 files changed, 96 insertions(+), 46 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index a591f12df90..bb85e62519b 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -6234,6 +6234,8 @@ const userSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target: const targetSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => target.status?.effect === StatusEffect.SLEEP || target.hasAbility(Abilities.COMATOSE); +const failIfLastCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => user.scene.phaseQueue.find(phase => phase instanceof MovePhase) !== undefined; + export type MoveAttrFilter = (attr: MoveAttr) => boolean; function applyMoveAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null, target: Pokemon | null, move: Move, args: any[]): Promise { @@ -6972,7 +6974,8 @@ export function initMoves() { .attr(StatusEffectAttr, StatusEffect.FREEZE) .target(MoveTarget.ALL_NEAR_ENEMIES), new SelfStatusMove(Moves.PROTECT, Type.NORMAL, -1, 10, -1, 4, 2) - .attr(ProtectAttr), + .attr(ProtectAttr) + .condition(failIfLastCondition), new AttackMove(Moves.MACH_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 40, 100, 30, -1, 1, 2) .punchingMove(), new StatusMove(Moves.SCARY_FACE, Type.NORMAL, 100, 10, -1, 0, 2) @@ -7023,7 +7026,8 @@ export function initMoves() { .windMove() .target(MoveTarget.ALL_NEAR_ENEMIES), new SelfStatusMove(Moves.DETECT, Type.FIGHTING, -1, 5, -1, 4, 2) - .attr(ProtectAttr), + .attr(ProtectAttr) + .condition(failIfLastCondition), new AttackMove(Moves.BONE_RUSH, Type.GROUND, MoveCategory.PHYSICAL, 25, 90, 10, -1, 0, 2) .attr(MultiHitAttr) .makesContact(false), @@ -7041,7 +7045,8 @@ export function initMoves() { .attr(HitHealAttr) .triageMove(), new SelfStatusMove(Moves.ENDURE, Type.NORMAL, -1, 10, -1, 4, 2) - .attr(ProtectAttr, BattlerTagType.ENDURING), + .attr(ProtectAttr, BattlerTagType.ENDURING) + .condition(failIfLastCondition), new StatusMove(Moves.CHARM, Type.FAIRY, 100, 20, -1, 0, 2) .attr(StatStageChangeAttr, [ Stat.ATK ], -2), new AttackMove(Moves.ROLLOUT, Type.ROCK, MoveCategory.PHYSICAL, 30, 90, 20, -1, 0, 2) @@ -7788,7 +7793,8 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.ATK, Stat.ACC ], 1, true), new StatusMove(Moves.WIDE_GUARD, Type.ROCK, -1, 10, -1, 3, 5) .target(MoveTarget.USER_SIDE) - .attr(AddArenaTagAttr, ArenaTagType.WIDE_GUARD, 1, true, true), + .attr(AddArenaTagAttr, ArenaTagType.WIDE_GUARD, 1, true, true) + .condition(failIfLastCondition), new StatusMove(Moves.GUARD_SPLIT, Type.PSYCHIC, -1, 10, -1, 0, 5) .attr(AverageStatsAttr, [ Stat.DEF, Stat.SPDEF ], "moveTriggers:sharedGuard"), new StatusMove(Moves.POWER_SPLIT, Type.PSYCHIC, -1, 10, -1, 0, 5) @@ -7876,7 +7882,8 @@ export function initMoves() { .attr(PositiveStatStagePowerAttr), new StatusMove(Moves.QUICK_GUARD, Type.FIGHTING, -1, 15, -1, 3, 5) .target(MoveTarget.USER_SIDE) - .attr(AddArenaTagAttr, ArenaTagType.QUICK_GUARD, 1, true, true), + .attr(AddArenaTagAttr, ArenaTagType.QUICK_GUARD, 1, true, true) + .condition(failIfLastCondition), new SelfStatusMove(Moves.ALLY_SWITCH, Type.PSYCHIC, -1, 15, -1, 2, 5) .ignoresProtect() .unimplemented(), @@ -8047,7 +8054,8 @@ export function initMoves() { new StatusMove(Moves.MAT_BLOCK, Type.FIGHTING, -1, 10, -1, 0, 6) .target(MoveTarget.USER_SIDE) .attr(AddArenaTagAttr, ArenaTagType.MAT_BLOCK, 1, true, true) - .condition(new FirstMoveCondition()), + .condition(new FirstMoveCondition()) + .condition(failIfLastCondition), 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, -1, 0, 6) @@ -8105,7 +8113,8 @@ export function initMoves() { .triageMove(), new StatusMove(Moves.CRAFTY_SHIELD, Type.FAIRY, -1, 10, -1, 3, 6) .target(MoveTarget.USER_SIDE) - .attr(AddArenaTagAttr, ArenaTagType.CRAFTY_SHIELD, 1, true, true), + .attr(AddArenaTagAttr, ArenaTagType.CRAFTY_SHIELD, 1, true, true) + .condition(failIfLastCondition), new StatusMove(Moves.FLOWER_SHIELD, Type.FAIRY, -1, 10, -1, 0, 6) .target(MoveTarget.ALL) .attr(StatStageChangeAttr, [ Stat.DEF ], 1, false, (user, target, move) => target.getTypes().includes(Type.GRASS) && !target.getTag(SemiInvulnerableTag)), @@ -8130,7 +8139,8 @@ export function initMoves() { .target(MoveTarget.BOTH_SIDES) .unimplemented(), new SelfStatusMove(Moves.KINGS_SHIELD, Type.STEEL, -1, 10, -1, 4, 6) - .attr(ProtectAttr, BattlerTagType.KINGS_SHIELD), + .attr(ProtectAttr, BattlerTagType.KINGS_SHIELD) + .condition(failIfLastCondition), new StatusMove(Moves.PLAY_NICE, Type.NORMAL, -1, 20, -1, 0, 6) .attr(StatStageChangeAttr, [ Stat.ATK ], -1), new StatusMove(Moves.CONFIDE, Type.NORMAL, -1, 20, -1, 0, 6) @@ -8153,7 +8163,8 @@ export function initMoves() { new AttackMove(Moves.MYSTICAL_FIRE, Type.FIRE, MoveCategory.SPECIAL, 75, 100, 10, 100, 0, 6) .attr(StatStageChangeAttr, [ Stat.SPATK ], -1), new SelfStatusMove(Moves.SPIKY_SHIELD, Type.GRASS, -1, 10, -1, 4, 6) - .attr(ProtectAttr, BattlerTagType.SPIKY_SHIELD), + .attr(ProtectAttr, BattlerTagType.SPIKY_SHIELD) + .condition(failIfLastCondition), new StatusMove(Moves.AROMATIC_MIST, Type.FAIRY, -1, 20, -1, 0, 6) .attr(StatStageChangeAttr, [ Stat.SPDEF ], 1) .target(MoveTarget.NEAR_ALLY), @@ -8349,7 +8360,8 @@ export function initMoves() { new AttackMove(Moves.FIRST_IMPRESSION, Type.BUG, MoveCategory.PHYSICAL, 90, 100, 10, -1, 2, 7) .condition(new FirstMoveCondition()), new SelfStatusMove(Moves.BANEFUL_BUNKER, Type.POISON, -1, 10, -1, 4, 7) - .attr(ProtectAttr, BattlerTagType.BANEFUL_BUNKER), + .attr(ProtectAttr, BattlerTagType.BANEFUL_BUNKER) + .condition(failIfLastCondition), new AttackMove(Moves.SPIRIT_SHACKLE, Type.GHOST, MoveCategory.PHYSICAL, 80, 100, 10, 100, 0, 7) .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1, 1, true) .makesContact(false), @@ -8592,6 +8604,7 @@ export function initMoves() { /* Unused */ new SelfStatusMove(Moves.MAX_GUARD, Type.NORMAL, -1, 10, -1, 4, 8) .attr(ProtectAttr) + .condition(failIfLastCondition) .ignoresVirtual(), /* End Unused */ new AttackMove(Moves.DYNAMAX_CANNON, Type.DRAGON, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 8) @@ -8770,7 +8783,8 @@ export function initMoves() { .target(MoveTarget.USER_AND_ALLIES) .ignoresProtect(), new SelfStatusMove(Moves.OBSTRUCT, Type.DARK, 100, 10, -1, 4, 8) - .attr(ProtectAttr, BattlerTagType.OBSTRUCT), + .attr(ProtectAttr, BattlerTagType.OBSTRUCT) + .condition(failIfLastCondition), new AttackMove(Moves.FALSE_SURRENDER, Type.DARK, MoveCategory.PHYSICAL, 80, -1, 10, -1, 0, 8), new AttackMove(Moves.METEOR_ASSAULT, Type.FIGHTING, MoveCategory.PHYSICAL, 150, 100, 5, -1, 0, 8) .attr(RechargeAttr) @@ -9061,7 +9075,8 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, true, (user, target, move) => user.isTerastallized() && user.isOfType(Type.STELLAR)) .partial(), new SelfStatusMove(Moves.SILK_TRAP, Type.BUG, -1, 10, -1, 4, 9) - .attr(ProtectAttr, BattlerTagType.SILK_TRAP), + .attr(ProtectAttr, BattlerTagType.SILK_TRAP) + .condition(failIfLastCondition), new AttackMove(Moves.AXE_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, 120, 90, 10, 30, 0, 9) .attr(MissEffectAttr, crashDamageFunc) .attr(NoEffectAttr, crashDamageFunc) @@ -9253,7 +9268,8 @@ export function initMoves() { .attr(PreMoveMessageAttr, doublePowerChanceMessageFunc) .attr(DoublePowerChanceAttr), new SelfStatusMove(Moves.BURNING_BULWARK, Type.FIRE, -1, 10, -1, 4, 9) - .attr(ProtectAttr, BattlerTagType.BURNING_BULWARK), + .attr(ProtectAttr, BattlerTagType.BURNING_BULWARK) + .condition(failIfLastCondition), new AttackMove(Moves.THUNDERCLAP, Type.ELECTRIC, MoveCategory.SPECIAL, 70, 100, 5, -1, 1, 9) .condition((user, target, move) => user.scene.currentBattle.turnCommands[target.getBattlerIndex()]?.command === Command.FIGHT && !target.turnData.acted && allMoves[user.scene.currentBattle.turnCommands[target.getBattlerIndex()]?.move?.move!].category !== MoveCategory.STATUS), // TODO: is this bang correct? new AttackMove(Moves.MIGHTY_CLEAVE, Type.ROCK, MoveCategory.PHYSICAL, 95, 100, 5, -1, 0, 9) diff --git a/src/test/moves/protect.test.ts b/src/test/moves/protect.test.ts index d792f586a37..83cd088aa47 100644 --- a/src/test/moves/protect.test.ts +++ b/src/test/moves/protect.test.ts @@ -7,7 +7,8 @@ import { Moves } from "#enums/moves"; import { Stat } from "#enums/stat"; import { allMoves } from "#app/data/move"; import { ArenaTagSide, ArenaTrapTag } from "#app/data/arena-tag"; -import { BerryPhase } from "#app/phases/berry-phase"; +import { BattlerIndex } from "#app/battle"; +import { MoveResult } from "#app/field/pokemon"; const TIMEOUT = 20 * 1000; @@ -43,13 +44,13 @@ describe("Moves - Protect", () => { test( "should protect the user from attacks", async () => { - await game.startBattle([Species.CHARIZARD]); + await game.classicMode.startBattle([Species.CHARIZARD]); const leadPokemon = game.scene.getPlayerPokemon()!; game.move.select(Moves.PROTECT); - await game.phaseInterceptor.to(BerryPhase, false); + await game.phaseInterceptor.to("BerryPhase", false); expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); }, TIMEOUT @@ -61,13 +62,13 @@ describe("Moves - Protect", () => { game.override.enemyMoveset(Array(4).fill(Moves.CEASELESS_EDGE)); vi.spyOn(allMoves[Moves.CEASELESS_EDGE], "accuracy", "get").mockReturnValue(100); - await game.startBattle([Species.CHARIZARD]); + await game.classicMode.startBattle([Species.CHARIZARD]); const leadPokemon = game.scene.getPlayerPokemon()!; game.move.select(Moves.PROTECT); - await game.phaseInterceptor.to(BerryPhase, false); + await game.phaseInterceptor.to("BerryPhase", false); expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); expect(game.scene.arena.getTagOnSide(ArenaTrapTag, ArenaTagSide.ENEMY)).toBeUndefined(); @@ -79,13 +80,13 @@ describe("Moves - Protect", () => { async () => { game.override.enemyMoveset(Array(4).fill(Moves.CHARM)); - await game.startBattle([Species.CHARIZARD]); + await game.classicMode.startBattle([Species.CHARIZARD]); const leadPokemon = game.scene.getPlayerPokemon()!; game.move.select(Moves.PROTECT); - await game.phaseInterceptor.to(BerryPhase, false); + await game.phaseInterceptor.to("BerryPhase", false); expect(leadPokemon.getStatStage(Stat.ATK)).toBe(0); }, TIMEOUT @@ -96,18 +97,38 @@ describe("Moves - Protect", () => { async () => { game.override.enemyMoveset(Array(4).fill(Moves.TACHYON_CUTTER)); - await game.startBattle([Species.CHARIZARD]); + await game.classicMode.startBattle([Species.CHARIZARD]); const leadPokemon = game.scene.getPlayerPokemon()!; - const enemyPokemon = game.scene.getEnemyPokemon()!; game.move.select(Moves.PROTECT); - await game.phaseInterceptor.to(BerryPhase, false); + await game.phaseInterceptor.to("BerryPhase", false); expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); expect(enemyPokemon.turnData.hitCount).toBe(1); }, TIMEOUT ); + + test( + "should fail if the user is the last to move in the turn", + async () => { + game.override.enemyMoveset(Array(4).fill(Moves.PROTECT)); + + await game.classicMode.startBattle([Species.CHARIZARD]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.PROTECT); + + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + + await game.phaseInterceptor.to("BerryPhase", false); + + expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); + expect(leadPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + }, TIMEOUT + ); }); diff --git a/src/test/moves/quick_guard.test.ts b/src/test/moves/quick_guard.test.ts index 25f98f8fa61..5f4af40eb71 100644 --- a/src/test/moves/quick_guard.test.ts +++ b/src/test/moves/quick_guard.test.ts @@ -5,8 +5,8 @@ import { Species } from "#enums/species"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Stat } from "#enums/stat"; -import { BerryPhase } from "#app/phases/berry-phase"; -import { CommandPhase } from "#app/phases/command-phase"; +import { BattlerIndex } from "#app/battle"; +import { MoveResult } from "#app/field/pokemon"; const TIMEOUT = 20 * 1000; @@ -42,19 +42,16 @@ describe("Moves - Quick Guard", () => { test( "should protect the user and allies from priority moves", async () => { - await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]); + await game.classicMode.startBattle([Species.CHARIZARD, Species.BLASTOISE]); - const leadPokemon = game.scene.getPlayerField(); + const playerPokemon = game.scene.getPlayerField(); game.move.select(Moves.QUICK_GUARD); - - await game.phaseInterceptor.to(CommandPhase); - game.move.select(Moves.SPLASH, 1); - await game.phaseInterceptor.to(BerryPhase, false); + await game.phaseInterceptor.to("BerryPhase", false); - leadPokemon.forEach(p => expect(p.hp).toBe(p.getMaxHp())); + playerPokemon.forEach(p => expect(p.hp).toBe(p.getMaxHp())); }, TIMEOUT ); @@ -64,19 +61,16 @@ describe("Moves - Quick Guard", () => { game.override.enemyAbility(Abilities.PRANKSTER); game.override.enemyMoveset(Array(4).fill(Moves.GROWL)); - await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]); + await game.classicMode.startBattle([Species.CHARIZARD, Species.BLASTOISE]); - const leadPokemon = game.scene.getPlayerField(); + const playerPokemon = game.scene.getPlayerField(); game.move.select(Moves.QUICK_GUARD); - - await game.phaseInterceptor.to(CommandPhase); - game.move.select(Moves.SPLASH, 1); - await game.phaseInterceptor.to(BerryPhase, false); + await game.phaseInterceptor.to("BerryPhase", false); - leadPokemon.forEach(p => expect(p.getStatStage(Stat.ATK)).toBe(0)); + playerPokemon.forEach(p => expect(p.getStatStage(Stat.ATK)).toBe(0)); }, TIMEOUT ); @@ -85,21 +79,40 @@ describe("Moves - Quick Guard", () => { async () => { game.override.enemyMoveset(Array(4).fill(Moves.WATER_SHURIKEN)); - await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]); + await game.classicMode.startBattle([Species.CHARIZARD, Species.BLASTOISE]); - const leadPokemon = game.scene.getPlayerField(); + const playerPokemon = game.scene.getPlayerField(); const enemyPokemon = game.scene.getEnemyField(); game.move.select(Moves.QUICK_GUARD); - - await game.phaseInterceptor.to(CommandPhase); - game.move.select(Moves.FOLLOW_ME, 1); - await game.phaseInterceptor.to(BerryPhase, false); + await game.phaseInterceptor.to("BerryPhase", false); - leadPokemon.forEach(p => expect(p.hp).toBe(p.getMaxHp())); + playerPokemon.forEach(p => expect(p.hp).toBe(p.getMaxHp())); enemyPokemon.forEach(p => expect(p.turnData.hitCount).toBe(1)); } ); + + test( + "should fail if the user is the last to move in the turn", + async () => { + game.override.battleType("single"); + game.override.enemyMoveset(Array(4).fill(Moves.QUICK_GUARD)); + + await game.classicMode.startBattle([Species.CHARIZARD]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.QUICK_GUARD); + + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + + await game.phaseInterceptor.to("BerryPhase", false); + + expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); + expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + }, TIMEOUT + ); });