[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 03445dfab4.

* added reviewed changes without changing public/locales

---------

Co-authored-by: ga27lok <geeil.han@tum.de>
This commit is contained in:
geeilhan 2024-11-13 16:41:39 +01:00 committed by GitHub
parent 162eea500d
commit 0c521bbe08
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 192 additions and 11 deletions

View File

@ -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()

View File

@ -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);
});
});