From cef2f2adf7f26abf4f93d6fa2ee0f670408146ae Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Sat, 30 Nov 2024 18:40:05 -0800 Subject: [PATCH] [Ability] Fully implement Sheer Force (#4890) * Added checks for Sheer Force interactions currently in the code. * Test for Relic Song interaction * Test for Shell Bell interaction * Created new Modifier class MoveEffectModifier * Applied new modifier class. * Revert "Applied new modifier class." This reverts commit 222bc8d42875485742ba8bd38fa5e9b978bbd53a. * Revert "Created new Modifier class MoveEffectModifier" This reverts commit 0e57ed03ff7c0e6fb59c7b3c2ea74b6fe6327f59. * Added checks for Shell Bell, Scope Lens, Wide Lens, Leek, and Golden Punch * Fixing function calls. * Fixed getSecondaryChanceMultiplier to just look at sheer force. * Rewrote old Sheer Force tests in accordance to current testing standards. * Resetting modifiers.ts * Update src/data/pokemon-forms.ts Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> * Moved getSecondaryChanceMultiplier to FlinchChanceModifier and revised Serene Grace tests * Adding an additional override to prevent test failures. * Removed Serene Grace factor from modifier. * Added forgotten conditional. * Added comment --------- Co-authored-by: frutescens Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> --- src/data/ability.ts | 4 +- src/data/pokemon-forms.ts | 4 + src/modifier/modifier.ts | 29 ++-- src/test/abilities/serene_grace.test.ts | 81 +++------ src/test/abilities/sheer_force.test.ts | 218 ++++++++++-------------- 5 files changed, 123 insertions(+), 213 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 234b502c23f..7fa046e2369 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -5713,9 +5713,7 @@ export function initAbilities() { .condition(getSheerForceHitDisableAbCondition()), new Ability(Abilities.SHEER_FORCE, 5) .attr(MovePowerBoostAbAttr, (user, target, move) => move.chance >= 1, 5461 / 4096) - .attr(MoveEffectChanceMultiplierAbAttr, 0) - .edgeCase() // Should disable shell bell and Meloetta's relic song transformation - .edgeCase(), // Should disable life orb, eject button, red card, kee/maranga berry if they get implemented + .attr(MoveEffectChanceMultiplierAbAttr, 0), // Should disable life orb, eject button, red card, kee/maranga berry if they get implemented new Ability(Abilities.CONTRARY, 5) .attr(StatStageChangeMultiplierAbAttr, -1) .ignorable(), diff --git a/src/data/pokemon-forms.ts b/src/data/pokemon-forms.ts index 2db0ed54294..a1b2d7896d7 100644 --- a/src/data/pokemon-forms.ts +++ b/src/data/pokemon-forms.ts @@ -351,6 +351,10 @@ export class MeloettaFormChangePostMoveTrigger extends SpeciesFormChangePostMove if (pokemon.scene.gameMode.hasChallenge(Challenges.SINGLE_TYPE)) { return false; } else { + // Meloetta will not transform if it has the ability Sheer Force when using Relic Song + if (pokemon.hasAbility(Abilities.SHEER_FORCE)) { + return false; + } return super.canChange(pokemon); } } diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 5e5246269a3..05d9e8b9897 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -18,7 +18,6 @@ import type { VoucherType } from "#app/system/voucher"; import { Command } from "#app/ui/command-ui-handler"; import { addTextObject, TextStyle } from "#app/ui/text"; import { BooleanHolder, hslToHex, isNullOrUndefined, NumberHolder, toDmgValue } from "#app/utils"; -import { Abilities } from "#enums/abilities"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; import { Moves } from "#enums/moves"; @@ -726,22 +725,6 @@ export abstract class PokemonHeldItemModifier extends PersistentModifier { return 1; } - //Applies to items with chance of activating secondary effects ie Kings Rock - getSecondaryChanceMultiplier(pokemon: Pokemon): number { - // Temporary quickfix to stop game from freezing when the opponet uses u-turn while holding on to king's rock - if (!pokemon.getLastXMoves()[0]) { - return 1; - } - const sheerForceAffected = allMoves[pokemon.getLastXMoves()[0].move].chance >= 0 && pokemon.hasAbility(Abilities.SHEER_FORCE); - - if (sheerForceAffected) { - return 0; - } else if (pokemon.hasAbility(Abilities.SERENE_GRACE)) { - return 2; - } - return 1; - } - getMaxStackCount(scene: BattleScene, forThreshold?: boolean): number { const pokemon = this.getPokemon(scene); if (!pokemon) { @@ -1614,9 +1597,16 @@ export class BypassSpeedChanceModifier extends PokemonHeldItemModifier { } } +/** + * Class for Pokemon held items like King's Rock + * Because King's Rock can be stacked in PokeRogue, unlike mainline, it does not receive a boost from Abilities.SERENE_GRACE + */ export class FlinchChanceModifier extends PokemonHeldItemModifier { + private chance: number; constructor(type: ModifierType, pokemonId: number, stackCount?: number) { super(type, pokemonId, stackCount); + + this.chance = 10; } matchType(modifier: Modifier) { @@ -1644,7 +1634,8 @@ export class FlinchChanceModifier extends PokemonHeldItemModifier { * @returns `true` if {@linkcode FlinchChanceModifier} has been applied */ override apply(pokemon: Pokemon, flinched: BooleanHolder): boolean { - if (!flinched.value && pokemon.randSeedInt(10) < (this.getStackCount() * this.getSecondaryChanceMultiplier(pokemon))) { + // The check for pokemon.battleSummonData is to ensure that a crash doesn't occur when a Pokemon with King's Rock procs a flinch + if (pokemon.battleSummonData && !flinched.value && pokemon.randSeedInt(100) < (this.getStackCount() * this.chance)) { flinched.value = true; return true; } @@ -1652,7 +1643,7 @@ export class FlinchChanceModifier extends PokemonHeldItemModifier { return false; } - getMaxHeldItemCount(pokemon: Pokemon): number { + getMaxHeldItemCount(_pokemon: Pokemon): number { return 3; } } diff --git a/src/test/abilities/serene_grace.test.ts b/src/test/abilities/serene_grace.test.ts index 3318c7fc27a..a19b5c82546 100644 --- a/src/test/abilities/serene_grace.test.ts +++ b/src/test/abilities/serene_grace.test.ts @@ -1,15 +1,12 @@ import { BattlerIndex } from "#app/battle"; -import { applyAbAttrs, MoveEffectChanceMultiplierAbAttr } from "#app/data/ability"; -import { Stat } from "#enums/stat"; -import { MoveEffectPhase } from "#app/phases/move-effect-phase"; -import * as Utils from "#app/utils"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; - +import { allMoves } from "#app/data/move"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { FlinchAttr } from "#app/data/move"; describe("Abilities - Serene Grace", () => { let phaserGame: Phaser.Game; @@ -27,66 +24,26 @@ describe("Abilities - Serene Grace", () => { beforeEach(() => { game = new GameManager(phaserGame); - const movesToUse = [ Moves.AIR_SLASH, Moves.TACKLE ]; - game.override.battleType("single"); - game.override.enemySpecies(Species.ONIX); - game.override.startingLevel(100); - game.override.moveset(movesToUse); - game.override.enemyMoveset([ Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE ]); + game.override + .battleType("single") + .ability(Abilities.SERENE_GRACE) + .moveset([ Moves.AIR_SLASH, Moves.TACKLE ]) + .enemyLevel(10) + .enemyMoveset([ Moves.SPLASH ]); }); - it("Move chance without Serene Grace", async () => { - const moveToUse = Moves.AIR_SLASH; - await game.startBattle([ - Species.PIDGEOT - ]); + it("Serene Grace should double the secondary effect chance of a move", async () => { + await game.classicMode.startBattle([ Species.SHUCKLE ]); + const airSlashMove = allMoves[Moves.AIR_SLASH]; + const airSlashFlinchAttr = airSlashMove.getAttrs(FlinchAttr)[0]; + vi.spyOn(airSlashFlinchAttr, "getMoveChance"); - game.scene.getEnemyParty()[0].stats[Stat.SPDEF] = 10000; - expect(game.scene.getPlayerParty()[0].formIndex).toBe(0); - - game.move.select(moveToUse); - + game.move.select(Moves.AIR_SLASH); await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); - await game.phaseInterceptor.to(MoveEffectPhase, false); + await game.move.forceHit(); + await game.phaseInterceptor.to("BerryPhase"); - // Check chance of Air Slash without Serene Grace - const phase = game.scene.getCurrentPhase() as MoveEffectPhase; - const move = phase.move.getMove(); - expect(move.id).toBe(Moves.AIR_SLASH); - - const chance = new Utils.IntegerHolder(move.chance); - console.log(move.chance + " Their ability is " + phase.getUserPokemon()!.getAbility().name); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); - expect(chance.value).toBe(30); - - }, 20000); - - it("Move chance with Serene Grace", async () => { - const moveToUse = Moves.AIR_SLASH; - game.override.ability(Abilities.SERENE_GRACE); - await game.startBattle([ - Species.TOGEKISS - ]); - - game.scene.getEnemyParty()[0].stats[Stat.SPDEF] = 10000; - expect(game.scene.getPlayerParty()[0].formIndex).toBe(0); - - game.move.select(moveToUse); - - await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); - await game.phaseInterceptor.to(MoveEffectPhase, false); - - // Check chance of Air Slash with Serene Grace - const phase = game.scene.getCurrentPhase() as MoveEffectPhase; - const move = phase.move.getMove(); - expect(move.id).toBe(Moves.AIR_SLASH); - - const chance = new Utils.IntegerHolder(move.chance); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); - expect(chance.value).toBe(60); - - }, 20000); - - //TODO King's Rock Interaction Unit Test + expect(airSlashFlinchAttr.getMoveChance).toHaveLastReturnedWith(60); + }); }); diff --git a/src/test/abilities/sheer_force.test.ts b/src/test/abilities/sheer_force.test.ts index 826694752b7..a0ddf5bb9c6 100644 --- a/src/test/abilities/sheer_force.test.ts +++ b/src/test/abilities/sheer_force.test.ts @@ -1,15 +1,13 @@ import { BattlerIndex } from "#app/battle"; -import { applyAbAttrs, applyPostDefendAbAttrs, applyPreAttackAbAttrs, MoveEffectChanceMultiplierAbAttr, MovePowerBoostAbAttr, PostDefendTypeChangeAbAttr } from "#app/data/ability"; -import { MoveEffectPhase } from "#app/phases/move-effect-phase"; -import { NumberHolder } from "#app/utils"; +import { Type } from "#app/enums/type"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { Stat } from "#enums/stat"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -import { allMoves } from "#app/data/move"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { allMoves, FlinchAttr } from "#app/data/move"; describe("Abilities - Sheer Force", () => { let phaserGame: Phaser.Game; @@ -27,143 +25,91 @@ describe("Abilities - Sheer Force", () => { beforeEach(() => { game = new GameManager(phaserGame); - const movesToUse = [ Moves.AIR_SLASH, Moves.BIND, Moves.CRUSH_CLAW, Moves.TACKLE ]; - game.override.battleType("single"); - game.override.enemySpecies(Species.ONIX); - game.override.startingLevel(100); - game.override.moveset(movesToUse); - game.override.enemyMoveset([ Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE ]); + game.override + .battleType("single") + .ability(Abilities.SHEER_FORCE) + .enemySpecies(Species.ONIX) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset([ Moves.SPLASH ]) + .disableCrits(); }); - it("Sheer Force", async () => { - const moveToUse = Moves.AIR_SLASH; - game.override.ability(Abilities.SHEER_FORCE); + const SHEER_FORCE_MULT = 5461 / 4096; + + it("Sheer Force should boost the power of the move but disable secondary effects", async () => { + game.override.moveset([ Moves.AIR_SLASH ]); + await game.classicMode.startBattle([ Species.SHUCKLE ]); + + const airSlashMove = allMoves[Moves.AIR_SLASH]; + vi.spyOn(airSlashMove, "calculateBattlePower"); + const airSlashFlinchAttr = airSlashMove.getAttrs(FlinchAttr)[0]; + vi.spyOn(airSlashFlinchAttr, "getMoveChance"); + + game.move.select(Moves.AIR_SLASH); + + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.move.forceHit(); + await game.phaseInterceptor.to("BerryPhase", false); + + expect(airSlashMove.calculateBattlePower).toHaveLastReturnedWith(airSlashMove.power * SHEER_FORCE_MULT); + expect(airSlashFlinchAttr.getMoveChance).toHaveLastReturnedWith(0); + }); + + it("Sheer Force does not affect the base damage or secondary effects of binding moves", async () => { + game.override.moveset([ Moves.BIND ]); + await game.classicMode.startBattle([ Species.SHUCKLE ]); + + const bindMove = allMoves[Moves.BIND]; + vi.spyOn(bindMove, "calculateBattlePower"); + + game.move.select(Moves.BIND); + + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.move.forceHit(); + await game.phaseInterceptor.to("BerryPhase", false); + + expect(bindMove.calculateBattlePower).toHaveLastReturnedWith(bindMove.power); + }, 20000); + + it("Sheer Force does not boost the base damage of moves with no secondary effect", async () => { + game.override.moveset([ Moves.TACKLE ]); await game.classicMode.startBattle([ Species.PIDGEOT ]); - game.scene.getEnemyPokemon()!.stats[Stat.SPDEF] = 10000; - expect(game.scene.getPlayerPokemon()!.formIndex).toBe(0); - - game.move.select(moveToUse); + const tackleMove = allMoves[Moves.TACKLE]; + vi.spyOn(tackleMove, "calculateBattlePower"); + game.move.select(Moves.TACKLE); await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); - await game.phaseInterceptor.to(MoveEffectPhase, false); + await game.move.forceHit(); + await game.phaseInterceptor.to("BerryPhase", false); - const phase = game.scene.getCurrentPhase() as MoveEffectPhase; - const move = phase.move.getMove(); - expect(move.id).toBe(Moves.AIR_SLASH); + expect(tackleMove.calculateBattlePower).toHaveLastReturnedWith(tackleMove.power); + }); - //Verify the move is boosted and has no chance of secondary effects - const power = new NumberHolder(move.power); - const chance = new NumberHolder(move.chance); + it("Sheer Force can disable the on-hit activation of specific abilities", async () => { + game.override + .moveset([ Moves.HEADBUTT ]) + .enemySpecies(Species.SQUIRTLE) + .enemyLevel(10) + .enemyAbility(Abilities.COLOR_CHANGE); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); - applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power); - - expect(chance.value).toBe(0); - expect(power.value).toBe(move.power * 5461 / 4096); - - - }, 20000); - - it("Sheer Force with exceptions including binding moves", async () => { - const moveToUse = Moves.BIND; - game.override.ability(Abilities.SHEER_FORCE); await game.classicMode.startBattle([ Species.PIDGEOT ]); + const enemyPokemon = game.scene.getEnemyPokemon(); + const headbuttMove = allMoves[Moves.HEADBUTT]; + vi.spyOn(headbuttMove, "calculateBattlePower"); + const headbuttFlinchAttr = headbuttMove.getAttrs(FlinchAttr)[0]; + vi.spyOn(headbuttFlinchAttr, "getMoveChance"); - - game.scene.getEnemyPokemon()!.stats[Stat.DEF] = 10000; - expect(game.scene.getPlayerPokemon()!.formIndex).toBe(0); - - game.move.select(moveToUse); + game.move.select(Moves.HEADBUTT); await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); - await game.phaseInterceptor.to(MoveEffectPhase, false); + await game.move.forceHit(); + await game.phaseInterceptor.to("BerryPhase", false); - const phase = game.scene.getCurrentPhase() as MoveEffectPhase; - const move = phase.move.getMove(); - expect(move.id).toBe(Moves.BIND); - - //Binding moves and other exceptions are not affected by Sheer Force and have a chance.value of -1 - const power = new NumberHolder(move.power); - const chance = new NumberHolder(move.chance); - - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); - applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power); - - expect(chance.value).toBe(-1); - expect(power.value).toBe(move.power); - - - }, 20000); - - it("Sheer Force with moves with no secondary effect", async () => { - const moveToUse = Moves.TACKLE; - game.override.ability(Abilities.SHEER_FORCE); - await game.classicMode.startBattle([ Species.PIDGEOT ]); - - - game.scene.getEnemyPokemon()!.stats[Stat.DEF] = 10000; - expect(game.scene.getPlayerPokemon()!.formIndex).toBe(0); - - game.move.select(moveToUse); - - await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); - await game.phaseInterceptor.to(MoveEffectPhase, false); - - const phase = game.scene.getCurrentPhase() as MoveEffectPhase; - const move = phase.move.getMove(); - expect(move.id).toBe(Moves.TACKLE); - - //Binding moves and other exceptions are not affected by Sheer Force and have a chance.value of -1 - const power = new NumberHolder(move.power); - const chance = new NumberHolder(move.chance); - - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); - applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power); - - expect(chance.value).toBe(-1); - expect(power.value).toBe(move.power); - - - }, 20000); - - it("Sheer Force Disabling Specific Abilities", async () => { - const moveToUse = Moves.CRUSH_CLAW; - game.override.enemyAbility(Abilities.COLOR_CHANGE); - game.override.startingHeldItems([{ name: "KINGS_ROCK", count: 1 }]); - game.override.ability(Abilities.SHEER_FORCE); - await game.startBattle([ Species.PIDGEOT ]); - - - game.scene.getEnemyPokemon()!.stats[Stat.DEF] = 10000; - expect(game.scene.getPlayerPokemon()!.formIndex).toBe(0); - - game.move.select(moveToUse); - - await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); - await game.phaseInterceptor.to(MoveEffectPhase, false); - - const phase = game.scene.getCurrentPhase() as MoveEffectPhase; - const move = phase.move.getMove(); - expect(move.id).toBe(Moves.CRUSH_CLAW); - - //Disable color change due to being hit by Sheer Force - const power = new NumberHolder(move.power); - const chance = new NumberHolder(move.chance); - const user = phase.getUserPokemon()!; - const target = phase.getFirstTarget()!; - const opponentType = target.getTypes()[0]; - - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, false, chance, move, target, false); - applyPreAttackAbAttrs(MovePowerBoostAbAttr, user, target, move, false, power); - applyPostDefendAbAttrs(PostDefendTypeChangeAbAttr, target, user, move, target.apply(user, move)); - - expect(chance.value).toBe(0); - expect(power.value).toBe(move.power * 5461 / 4096); - expect(target.getTypes().length).toBe(2); - expect(target.getTypes()[0]).toBe(opponentType); - - }, 20000); + expect(enemyPokemon?.getTypes()[0]).toBe(Type.WATER); + expect(headbuttMove.calculateBattlePower).toHaveLastReturnedWith(headbuttMove.power * SHEER_FORCE_MULT); + expect(headbuttFlinchAttr.getMoveChance).toHaveLastReturnedWith(0); + }); it("Two Pokemon with abilities disabled by Sheer Force hitting each other should not cause a crash", async () => { const moveToUse = Moves.CRUNCH; @@ -191,5 +137,19 @@ describe("Abilities - Sheer Force", () => { expect(onix.getTypes()).toStrictEqual(expectedTypes); }); - //TODO King's Rock Interaction Unit Test + it("Sheer Force should disable Meloetta's transformation from Relic Song", async () => { + game.override + .ability(Abilities.SHEER_FORCE) + .moveset([ Moves.RELIC_SONG ]) + .enemyMoveset([ Moves.SPLASH ]) + .enemyLevel(100); + await game.classicMode.startBattle([ Species.MELOETTA ]); + + const playerPokemon = game.scene.getPlayerPokemon(); + const formKeyStart = playerPokemon?.getFormKey(); + + game.move.select(Moves.RELIC_SONG); + await game.phaseInterceptor.to("TurnEndPhase"); + expect(formKeyStart).toBe(playerPokemon?.getFormKey()); + }); });