Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Move][Beta] Freeze-dry Re-implementation #4874

Merged
merged 1 commit into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/data/ability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ export class NonSuperEffectiveImmunityAbAttr extends TypeImmunityAbAttr {
applyPreDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean {
const modifierValue = args.length > 0
? (args[0] as Utils.NumberHolder).value
: pokemon.getAttackTypeEffectiveness(attacker.getMoveType(move), attacker);
: pokemon.getAttackTypeEffectiveness(attacker.getMoveType(move), attacker, undefined, undefined, move);

if (move instanceof AttackMove && modifierValue < 2) {
cancelled.value = true; // Suppresses "No Effect" message
Expand Down Expand Up @@ -3180,7 +3180,7 @@ function getAnticipationCondition(): AbAttrCondition {
continue;
}
// the move's base type (not accounting for variable type changes) is super effective
if (move.getMove() instanceof AttackMove && pokemon.getAttackTypeEffectiveness(move.getMove().type, opponent, true) >= 2) {
if (move.getMove() instanceof AttackMove && pokemon.getAttackTypeEffectiveness(move.getMove().type, opponent, true, undefined, move.getMove()) >= 2) {
return true;
}
// move is a OHKO
Expand Down
82 changes: 37 additions & 45 deletions src/data/move.ts
Original file line number Diff line number Diff line change
Expand Up @@ -897,7 +897,7 @@ export class AttackMove extends Move {

let attackScore = 0;

const effectiveness = target.getAttackTypeEffectiveness(this.type, user);
const effectiveness = target.getAttackTypeEffectiveness(this.type, user, undefined, undefined, this);
attackScore = Math.pow(effectiveness - 1, 2) * effectiveness < 1 ? -2 : 2;
if (attackScore) {
if (this.category === MoveCategory.PHYSICAL) {
Expand Down Expand Up @@ -4971,48 +4971,6 @@ export class NeutralDamageAgainstFlyingTypeMultiplierAttr extends VariableMoveTy
}
}

/**
* 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) && 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;
}
}

export class IceNoEffectTypeAttr extends VariableMoveTypeMultiplierAttr {
/**
* Checks to see if the Target is Ice-Type or not. If so, the move will have no effect.
Expand Down Expand Up @@ -5040,6 +4998,41 @@ export class FlyingTypeMultiplierAttr extends VariableMoveTypeMultiplierAttr {
}
}

/**
* Attribute for moves which have a custom type chart interaction.
*/
export class VariableMoveTypeChartAttr extends MoveAttr {
/**
* @param user {@linkcode Pokemon} using the move
* @param target {@linkcode Pokemon} target of the move
* @param move {@linkcode Move} with this attribute
* @param args [0] {@linkcode NumberHolder} holding the type effectiveness
* @param args [1] A single defensive type of the target
*
* @returns true if application of the attribute succeeds
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
return false;
}
}

/**
* This class forces Freeze-Dry to be super effective against Water Type.
*/
export class FreezeDryAttr extends VariableMoveTypeChartAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const multiplier = args[0] as Utils.NumberHolder;
const defType = args[1] as Type;

if (defType === Type.WATER) {
multiplier.value = 2;
return true;
} else {
return false;
}
}
}

export class OneHitKOAccuracyAttr extends VariableAccuracyAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const accuracy = args[0] as Utils.NumberHolder;
Expand Down Expand Up @@ -9448,8 +9441,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(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.
.attr(FreezeDryAttr),
new AttackMove(Moves.DISARMING_VOICE, Type.FAIRY, MoveCategory.SPECIAL, 40, -1, 15, -1, 0, 6)
.soundBased()
.target(MoveTarget.ALL_NEAR_ENEMIES),
Expand Down
10 changes: 7 additions & 3 deletions src/field/pokemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import BattleScene, { AnySound } from "#app/battle-scene";
import { Variant, VariantSet, variantColorCache } from "#app/data/variant";
import { variantData } from "#app/data/variant";
import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "#app/ui/battle-info";
import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget, CombinedPledgeStabBoostAttr } from "#app/data/move";
import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget, CombinedPledgeStabBoostAttr, VariableMoveTypeChartAttr } from "#app/data/move";
import { default as PokemonSpecies, PokemonSpeciesForm, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species";
import { CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER, getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters";
import { starterPassiveAbilities } from "#app/data/balance/passives";
Expand Down Expand Up @@ -1632,7 +1632,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const moveType = source.getMoveType(move);

const typeMultiplier = new Utils.NumberHolder((move.category !== MoveCategory.STATUS || move.hasAttr(RespectAttackTypeImmunityAttr))
? this.getAttackTypeEffectiveness(moveType, source, false, simulated)
? this.getAttackTypeEffectiveness(moveType, source, false, simulated, move)
: 1);

applyMoveAttrs(VariableMoveTypeMultiplierAttr, source, this, move, typeMultiplier);
Expand Down Expand Up @@ -1684,9 +1684,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* @param source {@linkcode Pokemon} the Pokemon using the move
* @param ignoreStrongWinds whether or not this ignores strong winds (anticipation, forewarn, stealth rocks)
* @param simulated tag to only apply the strong winds effect message when the move is used
* @param move (optional) the move whose type effectiveness is to be checked. Used for applying {@linkcode VariableMoveTypeChartAttr}
* @returns a multiplier for the type effectiveness
*/
getAttackTypeEffectiveness(moveType: Type, source?: Pokemon, ignoreStrongWinds: boolean = false, simulated: boolean = true): TypeDamageMultiplier {
getAttackTypeEffectiveness(moveType: Type, source?: Pokemon, ignoreStrongWinds: boolean = false, simulated: boolean = true, move?: Move): TypeDamageMultiplier {
if (moveType === Type.STELLAR) {
return this.isTerastallized() ? 2 : 1;
}
Expand All @@ -1705,6 +1706,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
let multiplier = types.map(defType => {
const multiplier = new Utils.NumberHolder(getTypeDamageMultiplier(moveType, defType));
applyChallenges(this.scene.gameMode, ChallengeType.TYPE_EFFECTIVENESS, multiplier);
if (move) {
applyMoveAttrs(VariableMoveTypeChartAttr, null, this, move, multiplier, defType);
}
if (source) {
const ignoreImmunity = new Utils.BooleanHolder(false);
if (source.isActive(true) && source.hasAbilityWithAttr(IgnoreTypeImmunityAbAttr)) {
Expand Down
68 changes: 67 additions & 1 deletion src/test/moves/freeze_dry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { Type } from "#enums/type";
import { Challenges } from "#enums/challenges";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
Expand Down Expand Up @@ -29,7 +30,7 @@ describe("Moves - Freeze-Dry", () => {
.enemyMoveset(Moves.SPLASH)
.starterSpecies(Species.FEEBAS)
.ability(Abilities.BALL_FETCH)
.moveset([ Moves.FREEZE_DRY ]);
.moveset([ Moves.FREEZE_DRY, Moves.FORESTS_CURSE, Moves.SOAK ]);
});

it("should deal 2x damage to pure water types", async () => {
Expand Down Expand Up @@ -98,6 +99,71 @@ describe("Moves - Freeze-Dry", () => {
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
});

it("should deal 8x damage to water/ground/grass type under Forest's Curse", async () => {
game.override.enemySpecies(Species.QUAGSIRE);
await game.classicMode.startBattle();

const enemy = game.scene.getEnemyPokemon()!;
vi.spyOn(enemy, "getMoveEffectiveness");

game.move.select(Moves.FORESTS_CURSE);
await game.toNextTurn();

game.move.select(Moves.FREEZE_DRY);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("MoveEffectPhase");

expect(enemy.getMoveEffectiveness).toHaveReturnedWith(8);
});

it("should deal 2x damage to steel type terastallized into water", async () => {
game.override.enemySpecies(Species.SKARMORY)
.enemyHeldItems([{ name: "TERA_SHARD", type: Type.WATER }]);
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(2);
});

it("should deal 0.5x damage to water type terastallized into fire", async () => {
game.override.enemySpecies(Species.PELIPPER)
.enemyHeldItems([{ name: "TERA_SHARD", type: Type.FIRE }]);
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.5);
});

it("should deal 0.5x damage to water type Terapagos with Tera Shell", async () => {
game.override.enemySpecies(Species.TERAPAGOS)
.enemyAbility(Abilities.TERA_SHELL);
await game.classicMode.startBattle();

const enemy = game.scene.getEnemyPokemon()!;
vi.spyOn(enemy, "getMoveEffectiveness");

game.move.select(Moves.SOAK);
await game.toNextTurn();

game.move.select(Moves.FREEZE_DRY);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("MoveEffectPhase");

expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0.5);
});

it("should deal 2x damage to water type under Normalize", async () => {
game.override.ability(Abilities.NORMALIZE);
await game.classicMode.startBattle();
Expand Down
Loading