From 9e018f4fe404f8807921e0c9fc89c376c76fec47 Mon Sep 17 00:00:00 2001 From: Mason Date: Tue, 20 Aug 2024 11:02:50 -0400 Subject: [PATCH] [Move] Implement Rage --- src/data/battler-tags.ts | 20 ++++++ src/data/move.ts | 2 +- src/enums/battler-tag-type.ts | 3 +- src/locales/en/battler-tags.ts | 1 + src/phases/move-effect-phase.ts | 3 + src/test/moves/rage.test.ts | 110 ++++++++++++++++++++++++++++++++ 6 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 src/test/moves/rage.test.ts diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index ede8d029327..934e5218de6 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -205,6 +205,24 @@ export class ShellTrapTag extends BattlerTag { } } +export class RageTag extends BattlerTag { + constructor() { + super(BattlerTagType.RAGE,[BattlerTagLapseType.MOVE_EFFECT],1,Moves.RAGE); + } + + lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { + if (lapseType === BattlerTagLapseType.MOVE_EFFECT) { + return (pokemon.scene.getCurrentPhase() as MovePhase).move.getMove().id === Moves.RAGE; + } else if (lapseType === BattlerTagLapseType.CUSTOM) { + pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene,pokemon.getBattlerIndex(),true,[BattleStat.ATK],1,false)); + pokemon.scene.queueMessage(i18next.t("battlerTags:rageOnHit", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon)})); + return true; + } + return false; + } +} + export class TrappedTag extends BattlerTag { constructor(tagType: BattlerTagType, lapseType: BattlerTagLapseType, turnCount: number, sourceMove: Moves, sourceId: number) { super(tagType, lapseType, turnCount, sourceMove, sourceId); @@ -1962,6 +1980,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source case BattlerTagType.GULP_MISSILE_ARROKUDA: case BattlerTagType.GULP_MISSILE_PIKACHU: return new GulpMissileTag(tagType, sourceMove); + case BattlerTagType.RAGE: + return new RageTag(); case BattlerTagType.NONE: default: return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId); diff --git a/src/data/move.ts b/src/data/move.ts index acb61042e70..f6de9e2ec09 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -6450,7 +6450,7 @@ export function initMoves() { .attr(StatChangeAttr, BattleStat.SPD, 2, true), new AttackMove(Moves.QUICK_ATTACK, Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 30, -1, 1, 1), new AttackMove(Moves.RAGE, Type.NORMAL, MoveCategory.PHYSICAL, 20, 100, 20, -1, 0, 1) - .partial(), + .attr(AddBattlerTagAttr,BattlerTagType.RAGE,true,false,0,0,false,true), new SelfStatusMove(Moves.TELEPORT, Type.PSYCHIC, -1, 20, -1, -6, 1) .attr(ForceSwitchOutAttr, true) .hidesUser(), diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index b133b442801..f1d1f1c887a 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -69,5 +69,6 @@ export enum BattlerTagType { GULP_MISSILE_ARROKUDA = "GULP_MISSILE_ARROKUDA", GULP_MISSILE_PIKACHU = "GULP_MISSILE_PIKACHU", BEAK_BLAST_CHARGING = "BEAK_BLAST_CHARGING", - SHELL_TRAP = "SHELL_TRAP" + SHELL_TRAP = "SHELL_TRAP", + RAGE = "RAGE" } diff --git a/src/locales/en/battler-tags.ts b/src/locales/en/battler-tags.ts index d0775efda08..002f26ea939 100644 --- a/src/locales/en/battler-tags.ts +++ b/src/locales/en/battler-tags.ts @@ -70,4 +70,5 @@ export const battlerTags: SimpleTranslationEntries = { "cursedOnAdd": "{{pokemonNameWithAffix}} cut its own HP and put a curse on the {{pokemonName}}!", "cursedLapse": "{{pokemonNameWithAffix}} is afflicted by the Curse!", "stockpilingOnAdd": "{{pokemonNameWithAffix}} stockpiled {{stockpiledCount}}!", + "rageOnHit": "{{pokemonNameWithAffix}}'s rage is building" } as const; diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index a5ac913cc5d..0cfe256f82a 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -262,6 +262,9 @@ export class MoveEffectPhase extends PokemonPhase { if (move.category === MoveCategory.PHYSICAL && user.isPlayer() !== target.isPlayer()) { target.lapseTag(BattlerTagType.SHELL_TRAP); } + if (hitResult < HitResult.NO_EFFECT && move.category !== MoveCategory.STATUS) { + target.lapseTag(BattlerTagType.RAGE); + } if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) { user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target); } diff --git a/src/test/moves/rage.test.ts b/src/test/moves/rage.test.ts new file mode 100644 index 00000000000..8d57006415d --- /dev/null +++ b/src/test/moves/rage.test.ts @@ -0,0 +1,110 @@ +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import GameManager from "#test/utils/gameManager"; +import { Species } from "#enums/species"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import {BattleStat} from "#app/data/battle-stat"; + +const TIMEOUT = 20 * 1000; + +describe("Moves - Rage", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .battleType("single") + .ability(Abilities.UNNERVE) + .moveset([Moves.RAGE,Moves.SPLASH,Moves.SPORE]) + .enemyAbility(Abilities.INSOMNIA) + .startingLevel(100) + .enemyLevel(100); + }); + + it( + "should raise attack if hit after use", + async () => { + game.override + .enemySpecies(Species.SHUCKLE) + .enemyMoveset([Moves.TACKLE,Moves.TACKLE,Moves.TACKLE,Moves.TACKLE]); + await game.startBattle([Species.NINJASK]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + + // Ninjask uses rage, then gets hit, gets atk boost + game.doAttack(0); + await game.toNextTurn(); + expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(1); + + }, TIMEOUT + ); + + it( + "should raise ATK if hit before using non-rage option", + async () => { + game.override + .enemySpecies(Species.NINJASK) + .enemyMoveset([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]); + await game.startBattle([Species.SHUCKLE]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + + // Ninjask moves first, THEN shuckle uses rage, no ATK boost + game.doAttack(0); + await game.toNextTurn(); + expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(0); + + // Shuckle Raged last turn, so when Ninjask hits it, ATK boost despite not using rage this turn + game.doAttack(1); + await game.toNextTurn(); + expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(1); + }, TIMEOUT + ); + + it( + "should not raise ATK if hit by status move", + async () => { + game.override + .enemySpecies(Species.NINJASK) + .enemyMoveset([Moves.RAGE, Moves.RAGE, Moves.RAGE, Moves.RAGE]); + await game.startBattle([Species.NINJASK]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + + // Ninjask Rages, then slept. No boost. + game.doAttack(2); + await game.toNextTurn(); + expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(0); + }, TIMEOUT + ); + + it( + "should not raise ATK if rage has no effect", + async () => { + game.override + .enemySpecies(Species.GASTLY) + .enemyMoveset([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]) + .moveset([Moves.RAGE]); + await game.startBattle([Species.NINJASK]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + + // Ninjask uses rage, but it has no effect, no ATK boost + game.doAttack(0); + await game.toNextTurn(); + expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(0); + }, TIMEOUT + ); +});