From e45cb42f7ee86899e45d9aa40aa390b65251dd76 Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Tue, 12 Nov 2024 18:42:47 -0800 Subject: [PATCH 1/3] [Balance] Disable King's Rock for moves that can already flinch (#4860) --- src/phases/move-effect-phase.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 24a0b51da96..afc8dd0475d 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -26,6 +26,7 @@ import { applyMoveAttrs, AttackMove, DelayedAttackAttr, + FlinchAttr, HitsTagAttr, MissEffectAttr, MoveAttr, @@ -502,6 +503,10 @@ export class MoveEffectPhase extends PokemonPhase { */ protected applyHeldItemFlinchCheck(user: Pokemon, target: Pokemon, dealsDamage: boolean) : () => void { return () => { + if (this.move.getMove().hasAttr(FlinchAttr)) { + return; + } + if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !this.move.getMove().hitsSubstitute(user, target)) { const flinched = new BooleanHolder(false); user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched); From 162eea500dcfaa5e39b06481339a60ebfb2d0c78 Mon Sep 17 00:00:00 2001 From: muscode Date: Wed, 13 Nov 2024 00:28:22 -0600 Subject: [PATCH 2/3] Fixed wild form changes messages, and form-changed Cramorant crashing the game when both sides faint at the same time (#4859) --- src/phases/quiet-form-change-phase.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/phases/quiet-form-change-phase.ts b/src/phases/quiet-form-change-phase.ts index c28cc28b592..6c84c0d1a8a 100644 --- a/src/phases/quiet-form-change-phase.ts +++ b/src/phases/quiet-form-change-phase.ts @@ -29,10 +29,14 @@ export class QuietFormChangePhase extends BattlePhase { const preName = getPokemonNameWithAffix(this.pokemon); - if (!this.pokemon.isOnField() || this.pokemon.getTag(SemiInvulnerableTag)) { - this.pokemon.changeForm(this.formChange).then(() => { - this.scene.ui.showText(getSpeciesFormChangeMessage(this.pokemon, this.formChange, preName), null, () => this.end(), 1500); - }); + if (!this.pokemon.isOnField() || this.pokemon.getTag(SemiInvulnerableTag) || this.pokemon.isFainted()) { + if (this.pokemon.isPlayer() || this.pokemon.isActive()) { + this.pokemon.changeForm(this.formChange).then(() => { + this.scene.ui.showText(getSpeciesFormChangeMessage(this.pokemon, this.formChange, preName), null, () => this.end(), 1500); + }); + } else { + this.end(); + } return; } From 0c521bbe0828746ace0dd7310340fb6340bee6e5 Mon Sep 17 00:00:00 2001 From: geeilhan <107366005+geeilhan@users.noreply.github.com> Date: Wed, 13 Nov 2024 16:41:39 +0100 Subject: [PATCH 3/3] [Move] Implement Freeze Dry type-changed interactions (#4840) * Full implementation of freeze-dry including edge cases such as Normalize and Electrify plus tests * Update comments * renamed WaterSuperEffectTypeMultiplierAttr to FreezeDryAttr * Added test case for freeze dry during inverse battles * cleaned up code making it more general * Added some more documentation * implementing reviewed changes * used getMoveType() instead of move.type * added additional test cases to freeze dry * Revert "used getMoveType() instead of move.type" This reverts commit 03445dfab4db52b0dddbe7abf7d4b4dfa8b9c583. * added reviewed changes without changing public/locales --------- Co-authored-by: ga27lok --- src/data/move.ts | 40 ++++++-- src/test/moves/freeze_dry.test.ts | 163 +++++++++++++++++++++++++++++- 2 files changed, 192 insertions(+), 11 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index 089bb51bf5e..74ac61af884 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -4971,16 +4971,42 @@ export class NeutralDamageAgainstFlyingTypeMultiplierAttr extends VariableMoveTy } } -export class WaterSuperEffectTypeMultiplierAttr extends VariableMoveTypeMultiplierAttr { +/** + * This class forces Freeze-Dry to be super effective against Water Type. + * It considers if target is Mono or Dual Type and calculates the new Multiplier accordingly. + * @see {@linkcode apply} + */ +export class FreezeDryAttr extends VariableMoveTypeMultiplierAttr { + /** + * If the target is Mono Type (Water only) then a 2x Multiplier is always forced. + * If target is Dual Type (containing Water) then only a 2x Multiplier is forced for the Water Type. + * + * Additionally Freeze-Dry's effectiveness against water is always forced during {@linkcode InverseBattleChallenge}. + * The multiplier is recalculated for the non-Water Type in case of Dual Type targets containing Water Type. + * + * @param user The {@linkcode Pokemon} applying the move + * @param target The {@linkcode Pokemon} targeted by the move + * @param move The move used by the user + * @param args `[0]` a {@linkcode Utils.NumberHolder | NumberHolder} containing a type effectiveness multiplier + * @returns `true` if super effectiveness on water type is forced; `false` otherwise + */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const multiplier = args[0] as Utils.NumberHolder; - if (target.isOfType(Type.WATER)) { - const effectivenessAgainstWater = new Utils.NumberHolder(getTypeDamageMultiplier(move.type, Type.WATER)); - applyChallenges(user.scene.gameMode, ChallengeType.TYPE_EFFECTIVENESS, effectivenessAgainstWater); - if (effectivenessAgainstWater.value !== 0) { - multiplier.value *= 2 / effectivenessAgainstWater.value; + if (target.isOfType(Type.WATER) && multiplier.value !== 0) { + const multipleTypes = (target.getTypes().length > 1); + + if (multipleTypes) { + const nonWaterType = target.getTypes().filter(type => type !== Type.WATER)[0]; + const effectivenessAgainstTarget = new Utils.NumberHolder(getTypeDamageMultiplier(user.getMoveType(move), nonWaterType)); + + applyChallenges(user.scene.gameMode, ChallengeType.TYPE_EFFECTIVENESS, effectivenessAgainstTarget); + + multiplier.value = effectivenessAgainstTarget.value * 2; return true; } + + multiplier.value = 2; + return true; } return false; @@ -9422,7 +9448,7 @@ export function initMoves() { .target(MoveTarget.ALL_NEAR_OTHERS), new AttackMove(Moves.FREEZE_DRY, Type.ICE, MoveCategory.SPECIAL, 70, 100, 20, 10, 0, 6) .attr(StatusEffectAttr, StatusEffect.FREEZE) - .attr(WaterSuperEffectTypeMultiplierAttr) + .attr(FreezeDryAttr) .edgeCase(), // This currently just multiplies the move's power instead of changing its effectiveness. It also doesn't account for abilities that modify type effectiveness such as tera shell. new AttackMove(Moves.DISARMING_VOICE, Type.FAIRY, MoveCategory.SPECIAL, 40, -1, 15, -1, 0, 6) .soundBased() diff --git a/src/test/moves/freeze_dry.test.ts b/src/test/moves/freeze_dry.test.ts index f766ed41a82..8bc6717f435 100644 --- a/src/test/moves/freeze_dry.test.ts +++ b/src/test/moves/freeze_dry.test.ts @@ -2,6 +2,7 @@ import { BattlerIndex } from "#app/battle"; import { Abilities } from "#app/enums/abilities"; import { Moves } from "#app/enums/moves"; import { Species } from "#app/enums/species"; +import { Challenges } from "#enums/challenges"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -97,8 +98,7 @@ describe("Moves - Freeze-Dry", () => { expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); }); - // enable if this is ever fixed (lol) - it.todo("should deal 2x damage to water types under Normalize", async () => { + it("should deal 2x damage to water type under Normalize", async () => { game.override.ability(Abilities.NORMALIZE); await game.classicMode.startBattle(); @@ -112,8 +112,39 @@ describe("Moves - Freeze-Dry", () => { expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2); }); - // enable once Electrify is implemented (and the interaction is fixed, as above) - it.todo("should deal 2x damage to water types under Electrify", async () => { + it("should deal 0.25x damage to rock/steel type under Normalize", async () => { + game.override + .ability(Abilities.NORMALIZE) + .enemySpecies(Species.SHIELDON); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0.25); + }); + + it("should deal 0x damage to water/ghost type under Normalize", async () => { + game.override + .ability(Abilities.NORMALIZE) + .enemySpecies(Species.JELLICENT); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0); + }); + + it("should deal 2x damage to water type under Electrify", async () => { game.override.enemyMoveset([ Moves.ELECTRIFY ]); await game.classicMode.startBattle(); @@ -126,4 +157,128 @@ describe("Moves - Freeze-Dry", () => { expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2); }); + + it("should deal 4x damage to water/flying type under Electrify", async () => { + game.override + .enemyMoveset([ Moves.ELECTRIFY ]) + .enemySpecies(Species.GYARADOS); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(4); + }); + + it("should deal 0x damage to water/ground type under Electrify", async () => { + game.override + .enemyMoveset([ Moves.ELECTRIFY ]) + .enemySpecies(Species.BARBOACH); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0); + }); + + it("should deal 0.25x damage to Grass/Dragon type under Electrify", async () => { + game.override + .enemyMoveset([ Moves.ELECTRIFY ]) + .enemySpecies(Species.FLAPPLE); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0.25); + }); + + it("should deal 2x damage to Water type during inverse battle", async () => { + game.override + .moveset([ Moves.FREEZE_DRY ]) + .enemySpecies(Species.MAGIKARP); + game.challengeMode.addChallenge(Challenges.INVERSE_BATTLE, 1, 1); + + + await game.challengeMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(enemy.getMoveEffectiveness).toHaveLastReturnedWith(2); + }); + + it("should deal 2x damage to Water type during inverse battle under Normalize", async () => { + game.override + .moveset([ Moves.FREEZE_DRY ]) + .ability(Abilities.NORMALIZE) + .enemySpecies(Species.MAGIKARP); + game.challengeMode.addChallenge(Challenges.INVERSE_BATTLE, 1, 1); + + await game.challengeMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(enemy.getMoveEffectiveness).toHaveLastReturnedWith(2); + }); + + it("should deal 2x damage to Water type during inverse battle under Electrify", async () => { + game.override + .moveset([ Moves.FREEZE_DRY ]) + .enemySpecies(Species.MAGIKARP) + .enemyMoveset([ Moves.ELECTRIFY ]); + game.challengeMode.addChallenge(Challenges.INVERSE_BATTLE, 1, 1); + + await game.challengeMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(enemy.getMoveEffectiveness).toHaveLastReturnedWith(2); + }); + + it("should deal 1x damage to water/flying type during inverse battle under Electrify", async () => { + game.override + .enemyMoveset([ Moves.ELECTRIFY ]) + .enemySpecies(Species.GYARADOS); + + game.challengeMode.addChallenge(Challenges.INVERSE_BATTLE, 1, 1); + + await game.challengeMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(1); + }); });