[Bug] Fix stat-protection-attribute not taking inverts (e.g. Contrary) into account (#4031)
* add generic types to`Pokemon.getAbilityAttrs()` * add invert check to `ProtectStatAbAttr.apply...` This makes sure that a stat is only protected if no other ability inverts the change. E.g. `Contrary` inverts any decrease to an increase * migrate contrary.test.ts to game.classicMode * move `StatStageChangeMultiplierAbAttr` resolve above `ProtectStatAbAttr` The effect of StatStageChangeMultiplierAbAttr is now applied before resolving any ProtectStatAbAttr. Thus the stage (level) of the BattleStat change was properly altered at the time of resolving the protection * revert ability.ts changes * add automated tests for `Clear Body` + `Contrary` * StateStageChangePhase replace ~~`IntegerHolder`~~ with `NumberHolder` Update Utils import and replace all occurcences of `Utils.` * contrary.test.ts: remove `js` import
This commit is contained in:
parent
00ba2eebc8
commit
4389bff5d0
|
@ -2625,7 +2625,11 @@ export class PreStatStageChangeAbAttr extends AbAttr {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protect one or all {@linkcode BattleStat} from reductions caused by other Pokémon's moves and Abilities
|
||||||
|
*/
|
||||||
export class ProtectStatAbAttr extends PreStatStageChangeAbAttr {
|
export class ProtectStatAbAttr extends PreStatStageChangeAbAttr {
|
||||||
|
/** {@linkcode BattleStat} to protect or `undefined` if **all** {@linkcode BattleStat} are protected */
|
||||||
private protectedStat?: BattleStat;
|
private protectedStat?: BattleStat;
|
||||||
|
|
||||||
constructor(protectedStat?: BattleStat) {
|
constructor(protectedStat?: BattleStat) {
|
||||||
|
@ -2634,7 +2638,17 @@ export class ProtectStatAbAttr extends PreStatStageChangeAbAttr {
|
||||||
this.protectedStat = protectedStat;
|
this.protectedStat = protectedStat;
|
||||||
}
|
}
|
||||||
|
|
||||||
applyPreStatStageChange(_pokemon: Pokemon, _passive: boolean, simulated: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, _args: any[]): boolean {
|
/**
|
||||||
|
* Apply the {@linkcode ProtectedStatAbAttr} to an interaction
|
||||||
|
* @param _pokemon
|
||||||
|
* @param _passive
|
||||||
|
* @param simulated
|
||||||
|
* @param stat the {@linkcode BattleStat} being affected
|
||||||
|
* @param cancelled The {@linkcode Utils.BooleanHolder} that will be set to true if the stat is protected
|
||||||
|
* @param _args
|
||||||
|
* @returns true if the stat is protected, false otherwise
|
||||||
|
*/
|
||||||
|
applyPreStatStageChange(_pokemon: Pokemon, _passive: boolean, _simulated: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, _args: any[]): boolean {
|
||||||
if (Utils.isNullOrUndefined(this.protectedStat) || stat === this.protectedStat) {
|
if (Utils.isNullOrUndefined(this.protectedStat) || stat === this.protectedStat) {
|
||||||
cancelled.value = true;
|
cancelled.value = true;
|
||||||
return true;
|
return true;
|
||||||
|
@ -3757,7 +3771,7 @@ export class StatStageChangeMultiplierAbAttr extends AbAttr {
|
||||||
this.multiplier = multiplier;
|
this.multiplier = multiplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean {
|
override apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean {
|
||||||
(args[0] as Utils.IntegerHolder).value *= this.multiplier;
|
(args[0] as Utils.IntegerHolder).value *= this.multiplier;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -1273,13 +1273,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||||
* @param attrType {@linkcode AbAttr} The ability attribute to check for.
|
* @param attrType {@linkcode AbAttr} The ability attribute to check for.
|
||||||
* @param canApply {@linkcode Boolean} If false, it doesn't check whether the ability is currently active
|
* @param canApply {@linkcode Boolean} If false, it doesn't check whether the ability is currently active
|
||||||
* @param ignoreOverride {@linkcode Boolean} If true, it ignores ability changing effects
|
* @param ignoreOverride {@linkcode Boolean} If true, it ignores ability changing effects
|
||||||
* @returns {AbAttr[]} A list of all the ability attributes on this ability.
|
* @returns A list of all the ability attributes on this ability.
|
||||||
*/
|
*/
|
||||||
getAbilityAttrs(attrType: { new(...args: any[]): AbAttr }, canApply: boolean = true, ignoreOverride?: boolean): AbAttr[] {
|
getAbilityAttrs<T extends AbAttr = AbAttr>(attrType: { new(...args: any[]): T }, canApply: boolean = true, ignoreOverride?: boolean): T[] {
|
||||||
const abilityAttrs: AbAttr[] = [];
|
const abilityAttrs: T[] = [];
|
||||||
|
|
||||||
if (!canApply || this.canApplyAbility()) {
|
if (!canApply || this.canApplyAbility()) {
|
||||||
abilityAttrs.push(...this.getAbility(ignoreOverride).getAttrs(attrType));
|
abilityAttrs.push(...this.getAbility(ignoreOverride).getAttrs<T>(attrType));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!canApply || this.canApplyAbility(true)) {
|
if (!canApply || this.canApplyAbility(true)) {
|
||||||
|
|
|
@ -6,7 +6,7 @@ import Pokemon from "#app/field/pokemon";
|
||||||
import { getPokemonNameWithAffix } from "#app/messages";
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
import { ResetNegativeStatStageModifier } from "#app/modifier/modifier";
|
import { ResetNegativeStatStageModifier } from "#app/modifier/modifier";
|
||||||
import { handleTutorial, Tutorial } from "#app/tutorial";
|
import { handleTutorial, Tutorial } from "#app/tutorial";
|
||||||
import * as Utils from "#app/utils";
|
import { NumberHolder, BooleanHolder } from "#app/utils";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import { PokemonPhase } from "./pokemon-phase";
|
import { PokemonPhase } from "./pokemon-phase";
|
||||||
import { Stat, type BattleStat, getStatKey, getStatStageChangeDescriptionKey } from "#enums/stat";
|
import { Stat, type BattleStat, getStatKey, getStatStageChangeDescriptionKey } from "#enums/stat";
|
||||||
|
@ -42,17 +42,23 @@ export class StatStageChangePhase extends PokemonPhase {
|
||||||
return this.end();
|
return this.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stages = new NumberHolder(this.stages);
|
||||||
|
|
||||||
|
if (!this.ignoreAbilities) {
|
||||||
|
applyAbAttrs(StatStageChangeMultiplierAbAttr, pokemon, null, false, stages);
|
||||||
|
}
|
||||||
|
|
||||||
let simulate = false;
|
let simulate = false;
|
||||||
|
|
||||||
const filteredStats = this.stats.filter(stat => {
|
const filteredStats = this.stats.filter(stat => {
|
||||||
const cancelled = new Utils.BooleanHolder(false);
|
const cancelled = new BooleanHolder(false);
|
||||||
|
|
||||||
if (!this.selfTarget && this.stages < 0) {
|
if (!this.selfTarget && stages.value < 0) {
|
||||||
// TODO: Include simulate boolean when tag applications can be simulated
|
// TODO: Include simulate boolean when tag applications can be simulated
|
||||||
this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, cancelled);
|
this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, cancelled);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cancelled.value && !this.selfTarget && this.stages < 0) {
|
if (!cancelled.value && !this.selfTarget && stages.value < 0) {
|
||||||
applyPreStatStageChangeAbAttrs(ProtectStatAbAttr, pokemon, stat, cancelled, simulate);
|
applyPreStatStageChangeAbAttrs(ProtectStatAbAttr, pokemon, stat, cancelled, simulate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,12 +70,6 @@ export class StatStageChangePhase extends PokemonPhase {
|
||||||
return !cancelled.value;
|
return !cancelled.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
const stages = new Utils.IntegerHolder(this.stages);
|
|
||||||
|
|
||||||
if (!this.ignoreAbilities) {
|
|
||||||
applyAbAttrs(StatStageChangeMultiplierAbAttr, pokemon, null, false, stages);
|
|
||||||
}
|
|
||||||
|
|
||||||
const relLevels = filteredStats.map(s => (stages.value >= 1 ? Math.min(pokemon.getStatStage(s) + stages.value, 6) : Math.max(pokemon.getStatStage(s) + stages.value, -6)) - pokemon.getStatStage(s));
|
const relLevels = filteredStats.map(s => (stages.value >= 1 ? Math.min(pokemon.getStatStage(s) + stages.value, 6) : Math.max(pokemon.getStatStage(s) + stages.value, -6)) - pokemon.getStatStage(s));
|
||||||
|
|
||||||
this.onChange && this.onChange(this.getPokemon(), filteredStats, relLevels);
|
this.onChange && this.onChange(this.getPokemon(), filteredStats, relLevels);
|
||||||
|
|
|
@ -31,7 +31,7 @@ describe("Abilities - Contrary", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should invert stat changes when applied", async() => {
|
it("should invert stat changes when applied", async() => {
|
||||||
await game.startBattle([
|
await game.classicMode.startBattle([
|
||||||
Species.SLOWBRO
|
Species.SLOWBRO
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -39,4 +39,39 @@ describe("Abilities - Contrary", () => {
|
||||||
|
|
||||||
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
|
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
|
||||||
}, 20000);
|
}, 20000);
|
||||||
|
|
||||||
|
describe("With Clear Body", () => {
|
||||||
|
it("should apply positive effects", async () => {
|
||||||
|
game.override
|
||||||
|
.enemyPassiveAbility(Abilities.CLEAR_BODY)
|
||||||
|
.moveset([Moves.TAIL_WHIP]);
|
||||||
|
await game.classicMode.startBattle([Species.SLOWBRO]);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||||
|
|
||||||
|
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
|
||||||
|
|
||||||
|
game.move.select(Moves.TAIL_WHIP);
|
||||||
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
|
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should block negative effects", async () => {
|
||||||
|
game.override
|
||||||
|
.enemyPassiveAbility(Abilities.CLEAR_BODY)
|
||||||
|
.enemyMoveset([Moves.HOWL, Moves.HOWL, Moves.HOWL, Moves.HOWL])
|
||||||
|
.moveset([Moves.SPLASH]);
|
||||||
|
await game.classicMode.startBattle([Species.SLOWBRO]);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||||
|
|
||||||
|
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
|
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue