From f3256ec5d4c1a900d093768977374464f32339cb Mon Sep 17 00:00:00 2001 From: geeilhan <107366005+geeilhan@users.noreply.github.com> Date: Sun, 19 Jan 2025 18:52:50 +0100 Subject: [PATCH] [Move] Improve implementation of Rage Fist damage increase (#5129) * implementation of rage fist * Apply suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update src/test/moves/rage_fist.test.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * added removed TODO from some test cases * Apply suggestions from code review Added changes to documentation and cleaning up code Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * added protected to updateHitReceivedCount() --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: damocleas --- src/data/custom-pokemon-data.ts | 10 +- src/data/move.ts | 28 ++++- src/field/pokemon.ts | 3 + src/phases/encounter-phase.ts | 6 + src/phases/new-biome-encounter-phase.ts | 1 + src/test/moves/rage_fist.test.ts | 143 ++++++++++++++++++++++++ 6 files changed, 186 insertions(+), 5 deletions(-) create mode 100644 src/test/moves/rage_fist.test.ts diff --git a/src/data/custom-pokemon-data.ts b/src/data/custom-pokemon-data.ts index 1c3bbbc31801..4a5eb89aeed4 100644 --- a/src/data/custom-pokemon-data.ts +++ b/src/data/custom-pokemon-data.ts @@ -5,7 +5,8 @@ import type { Nature } from "#enums/nature"; /** * Data that can customize a Pokemon in non-standard ways from its Species - * Currently only used by Mystery Encounters and Mints. + * Used by Mystery Encounters and Mints + * Also used as a counter how often a Pokemon got hit until new arena encounter */ export class CustomPokemonData { public spriteScale: number; @@ -13,6 +14,8 @@ export class CustomPokemonData { public passive: Abilities | -1; public nature: Nature | -1; public types: Type[]; + /** `hitsReceivedCount` aka `hitsRecCount` saves how often the pokemon got hit until a new arena encounter (used for Rage Fist) */ + public hitsRecCount: number; constructor(data?: CustomPokemonData | Partial) { if (!isNullOrUndefined(data)) { @@ -24,5 +27,10 @@ export class CustomPokemonData { this.passive = this.passive ?? -1; this.nature = this.nature ?? -1; this.types = this.types ?? []; + this.hitsRecCount = this.hitsRecCount ?? 0; + } + + resetHitReceivedCount(): void { + this.hitsRecCount = 0; } } diff --git a/src/data/move.ts b/src/data/move.ts index 572fbf4c2acf..06f3c85e9c42 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -3993,12 +3993,32 @@ export class FriendshipPowerAttr extends VariablePowerAttr { } } -export class HitCountPowerAttr extends VariablePowerAttr { +/** + * This Attribute calculates the current power of {@linkcode Moves.RAGE_FIST}. + * The counter for power calculation does not reset on every wave but on every new arena encounter + */ +export class RageFistPowerAttr extends VariablePowerAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - (args[0] as Utils.NumberHolder).value += Math.min(user.battleData.hitCount, 6) * 50; + const { hitCount, prevHitCount } = user.battleData; + const basePower: Utils.NumberHolder = args[0]; + + this.updateHitReceivedCount(user, hitCount, prevHitCount); + + basePower.value = 50 + (Math.min(user.customPokemonData.hitsRecCount, 6) * 50); return true; } + + /** + * Updates the number of hits the Pokemon has taken in battle + * @param user Pokemon calling Rage Fist + * @param hitCount The number of received hits this battle + * @param previousHitCount The number of received hits this battle since last time Rage Fist was used + */ + protected updateHitReceivedCount(user: Pokemon, hitCount: number, previousHitCount: number): void { + user.customPokemonData.hitsRecCount += (hitCount - previousHitCount); + user.battleData.prevHitCount = hitCount; + } } /** @@ -10991,8 +11011,8 @@ export function initMoves() { new AttackMove(Moves.TWIN_BEAM, Type.PSYCHIC, MoveCategory.SPECIAL, 40, 100, 10, -1, 0, 9) .attr(MultiHitAttr, MultiHitType._2), new AttackMove(Moves.RAGE_FIST, Type.GHOST, MoveCategory.PHYSICAL, 50, 100, 10, -1, 0, 9) - .partial() // Counter resets every wave instead of on arena reset - .attr(HitCountPowerAttr) + .edgeCase() // Counter incorrectly increases on confusion self-hits + .attr(RageFistPowerAttr) .punchingMove(), new AttackMove(Moves.ARMOR_CANNON, Type.FIRE, MoveCategory.SPECIAL, 120, 100, 5, -1, 0, 9) .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], -1, true), diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 432f0a92fec8..a833facd2f89 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -5282,7 +5282,10 @@ export class PokemonSummonData { } export class PokemonBattleData { + /** counts the hits the pokemon received */ public hitCount: number = 0; + /** used for {@linkcode Moves.RAGE_FIST} in order to save hit Counts received before Rage Fist is applied */ + public prevHitCount: number = 0; public endured: boolean = false; public berriesEaten: BerryType[] = []; public abilitiesApplied: Abilities[] = []; diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index 6dae7dff8f93..353dd6681cbf 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -104,6 +104,12 @@ export class EncounterPhase extends BattlePhase { } if (!this.loaded) { if (battle.battleType === BattleType.TRAINER) { + //resets hitRecCount during Trainer ecnounter + for (const pokemon of globalScene.getPlayerParty()) { + if (pokemon) { + pokemon.customPokemonData.resetHitReceivedCount(); + } + } battle.enemyParty[e] = battle.trainer?.genPartyMember(e)!; // TODO:: is the bang correct here? } else { let enemySpecies = globalScene.randomSpecies(battle.waveIndex, level, true); diff --git a/src/phases/new-biome-encounter-phase.ts b/src/phases/new-biome-encounter-phase.ts index be6815333e55..2de9a4300c59 100644 --- a/src/phases/new-biome-encounter-phase.ts +++ b/src/phases/new-biome-encounter-phase.ts @@ -14,6 +14,7 @@ export class NewBiomeEncounterPhase extends NextEncounterPhase { for (const pokemon of globalScene.getPlayerParty()) { if (pokemon) { pokemon.resetBattleData(); + pokemon.customPokemonData.resetHitReceivedCount(); } } diff --git a/src/test/moves/rage_fist.test.ts b/src/test/moves/rage_fist.test.ts new file mode 100644 index 000000000000..a85be5a88d9f --- /dev/null +++ b/src/test/moves/rage_fist.test.ts @@ -0,0 +1,143 @@ +import { BattlerIndex } from "#app/battle"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { allMoves } from "#app/data/move"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Moves - Rage Fist", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + const move = allMoves[Moves.RAGE_FIST]; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .battleType("single") + .moveset([ Moves.RAGE_FIST, Moves.SPLASH, Moves.SUBSTITUTE ]) + .startingLevel(100) + .enemyLevel(1) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.DOUBLE_KICK); + + vi.spyOn(move, "calculateBattlePower"); + }); + + it("should have 100 more power if hit twice before calling Rage Fist", async () => { + game.override + .enemySpecies(Species.MAGIKARP); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + game.move.select(Moves.RAGE_FIST); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(move.calculateBattlePower).toHaveLastReturnedWith(150); + }); + + it("should maintain its power during next battle if it is within the same arena encounter", async () => { + game.override + .enemySpecies(Species.MAGIKARP) + .startingWave(1); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + game.move.select(Moves.RAGE_FIST); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextWave(); + + game.move.select(Moves.RAGE_FIST); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase", false); + + expect(move.calculateBattlePower).toHaveLastReturnedWith(250); + }); + + it("should reset the hitRecCounter if we enter new trainer battle", async () => { + game.override + .enemySpecies(Species.MAGIKARP) + .startingWave(4); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + game.move.select(Moves.RAGE_FIST); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextWave(); + + game.move.select(Moves.RAGE_FIST); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase", false); + + expect(move.calculateBattlePower).toHaveLastReturnedWith(150); + }); + + it("should not increase the hitCounter if Substitute is hit", async () => { + game.override + .enemySpecies(Species.MAGIKARP) + .startingWave(4); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + game.move.select(Moves.SUBSTITUTE); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(game.scene.getPlayerPokemon()?.customPokemonData.hitsRecCount).toBe(0); + }); + + it("should reset the hitRecCounter if we enter new biome", async () => { + game.override + .enemySpecies(Species.MAGIKARP) + .startingWave(10); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + game.move.select(Moves.RAGE_FIST); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextTurn(); + + game.move.select(Moves.RAGE_FIST); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase", false); + + expect(move.calculateBattlePower).toHaveLastReturnedWith(150); + }); + + it("should not reset the hitRecCounter if switched out", async () => { + game.override + .enemySpecies(Species.MAGIKARP) + .startingWave(1) + .enemyMoveset(Moves.TACKLE); + + await game.classicMode.startBattle([ Species.CHARIZARD, Species.BLASTOISE ]); + + game.move.select(Moves.SPLASH); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextTurn(); + + game.doSwitchPokemon(1); + await game.toNextTurn(); + + game.doSwitchPokemon(1); + await game.toNextTurn(); + + game.move.select(Moves.RAGE_FIST); + await game.phaseInterceptor.to("MoveEndPhase"); + + expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(Species.CHARIZARD); + expect(move.calculateBattlePower).toHaveLastReturnedWith(150); + }); +});