[Bug] Fix substitute interactions with PostDefendAbAttrs (#4570)

* Fixes some Substitute interactions

Specifically with Disguise/Ice Face and Gulp Missile

* Add tests

* Fix linting

* Add `hitsSubstitute()` checks to all `PostDefendAbAttr`s

Also fix comment indentation in `MoveEffectPhase`

* Revert `move-effect-phase.ts` changes
This commit is contained in:
NightKev 2024-10-10 08:45:02 -07:00 committed by GitHub
parent a778537cca
commit ba7e26152e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 152 additions and 89 deletions

View File

@ -634,15 +634,15 @@ export class ReverseDrainAbAttr extends PostDefendAbAttr {
* Examples include: Absorb, Draining Kiss, Bitter Blade, etc.
* Also displays a message to show this ability was activated.
* @param pokemon {@linkcode Pokemon} with this ability
* @param passive N/A
* @param _passive N/A
* @param attacker {@linkcode Pokemon} that is attacking this Pokemon
* @param move {@linkcode PokemonMove} that is being used
* @param hitResult N/A
* @args N/A
* @param _hitResult N/A
* @param _args N/A
* @returns true if healing should be reversed on a healing move, false otherwise.
*/
applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
if (move.hasAttr(HitHealAttr)) {
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): boolean {
if (move.hasAttr(HitHealAttr) && !move.hitsSubstitute(attacker, pokemon)) {
if (!simulated) {
pokemon.scene.queueMessage(i18next.t("abilityTriggers:reverseDrain", { pokemonNameWithAffix: getPokemonNameWithAffix(attacker) }));
}
@ -669,8 +669,8 @@ export class PostDefendStatStageChangeAbAttr extends PostDefendAbAttr {
this.allOthers = allOthers;
}
applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
if (this.condition(pokemon, attacker, move)) {
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): boolean {
if (this.condition(pokemon, attacker, move) && !move.hitsSubstitute(attacker, pokemon)) {
if (simulated) {
return true;
}
@ -707,13 +707,13 @@ export class PostDefendHpGatedStatStageChangeAbAttr extends PostDefendAbAttr {
this.selfTarget = selfTarget;
}
applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
const hpGateFlat: integer = Math.ceil(pokemon.getMaxHp() * this.hpGate);
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): boolean {
const hpGateFlat: number = Math.ceil(pokemon.getMaxHp() * this.hpGate);
const lastAttackReceived = pokemon.turnData.attacksReceived[pokemon.turnData.attacksReceived.length - 1];
const damageReceived = lastAttackReceived?.damage || 0;
if (this.condition(pokemon, attacker, move) && (pokemon.hp <= hpGateFlat && (pokemon.hp + damageReceived) > hpGateFlat)) {
if (!simulated ) {
if (this.condition(pokemon, attacker, move) && (pokemon.hp <= hpGateFlat && (pokemon.hp + damageReceived) > hpGateFlat) && !move.hitsSubstitute(attacker, pokemon)) {
if (!simulated) {
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, (this.selfTarget ? pokemon : attacker).getBattlerIndex(), true, this.stats, this.stages));
}
return true;
@ -734,8 +734,8 @@ export class PostDefendApplyArenaTrapTagAbAttr extends PostDefendAbAttr {
this.tagType = tagType;
}
applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
if (this.condition(pokemon, attacker, move)) {
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): boolean {
if (this.condition(pokemon, attacker, move) && !move.hitsSubstitute(attacker, pokemon)) {
const tag = pokemon.scene.arena.getTag(this.tagType) as ArenaTrapTag;
if (!pokemon.scene.arena.getTag(this.tagType) || tag.layers < tag.maxLayers) {
if (!simulated) {
@ -758,8 +758,8 @@ export class PostDefendApplyBattlerTagAbAttr extends PostDefendAbAttr {
this.tagType = tagType;
}
applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
if (this.condition(pokemon, attacker, move)) {
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): boolean {
if (this.condition(pokemon, attacker, move) && !move.hitsSubstitute(attacker, pokemon)) {
if (!pokemon.getTag(this.tagType) && !simulated) {
pokemon.addTag(this.tagType, undefined, undefined, pokemon.id);
pokemon.scene.queueMessage(i18next.t("abilityTriggers:windPowerCharged", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: move.name }));
@ -771,8 +771,8 @@ export class PostDefendApplyBattlerTagAbAttr extends PostDefendAbAttr {
}
export class PostDefendTypeChangeAbAttr extends PostDefendAbAttr {
applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
if (hitResult < HitResult.NO_EFFECT) {
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, _args: any[]): boolean {
if (hitResult < HitResult.NO_EFFECT && !move.hitsSubstitute(attacker, pokemon)) {
if (simulated) {
return true;
}
@ -787,7 +787,7 @@ export class PostDefendTypeChangeAbAttr extends PostDefendAbAttr {
return false;
}
getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string {
override getTriggerMessage(pokemon: Pokemon, abilityName: string, ..._args: any[]): string {
return i18next.t("abilityTriggers:postDefendTypeChange", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
abilityName,
@ -805,8 +805,8 @@ export class PostDefendTerrainChangeAbAttr extends PostDefendAbAttr {
this.terrainType = terrainType;
}
applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
if (hitResult < HitResult.NO_EFFECT) {
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, _args: any[]): boolean {
if (hitResult < HitResult.NO_EFFECT && !move.hitsSubstitute(attacker, pokemon)) {
if (simulated) {
return pokemon.scene.arena.terrain?.terrainType !== (this.terrainType || undefined);
} else {
@ -829,8 +829,9 @@ export class PostDefendContactApplyStatusEffectAbAttr extends PostDefendAbAttr {
this.effects = effects;
}
applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && !attacker.status && (this.chance === -1 || pokemon.randSeedInt(100) < this.chance)) {
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): boolean {
if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && !attacker.status
&& (this.chance === -1 || pokemon.randSeedInt(100) < this.chance) && !move.hitsSubstitute(attacker, pokemon)) {
const effect = this.effects.length === 1 ? this.effects[0] : this.effects[pokemon.randSeedInt(this.effects.length)];
if (simulated) {
return attacker.canSetStatus(effect, true, false, pokemon);
@ -869,8 +870,8 @@ export class PostDefendContactApplyTagChanceAbAttr extends PostDefendAbAttr {
this.turnCount = turnCount;
}
applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && pokemon.randSeedInt(100) < this.chance) {
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): boolean {
if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && pokemon.randSeedInt(100) < this.chance && !move.hitsSubstitute(attacker, pokemon)) {
if (simulated) {
return attacker.canAddTag(this.tagType);
} else {
@ -893,7 +894,11 @@ export class PostDefendCritStatStageChangeAbAttr extends PostDefendAbAttr {
this.stages = stages;
}
applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): boolean {
if (move.hitsSubstitute(attacker, pokemon)) {
return false;
}
if (!simulated) {
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ this.stat ], this.stages));
}
@ -901,7 +906,7 @@ export class PostDefendCritStatStageChangeAbAttr extends PostDefendAbAttr {
return true;
}
getCondition(): AbAttrCondition {
override getCondition(): AbAttrCondition {
return (pokemon: Pokemon) => pokemon.turnData.attacksReceived.length !== 0 && pokemon.turnData.attacksReceived[pokemon.turnData.attacksReceived.length - 1].critical;
}
}
@ -915,8 +920,9 @@ export class PostDefendContactDamageAbAttr extends PostDefendAbAttr {
this.damageRatio = damageRatio;
}
applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
if (!simulated && move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && !attacker.hasAbilityWithAttr(BlockNonDirectDamageAbAttr)) {
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): boolean {
if (!simulated && move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon)
&& !attacker.hasAbilityWithAttr(BlockNonDirectDamageAbAttr) && !move.hitsSubstitute(attacker, pokemon)) {
attacker.damageAndUpdate(Utils.toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)), HitResult.OTHER);
attacker.turnData.damageTaken += Utils.toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio));
return true;
@ -925,7 +931,7 @@ export class PostDefendContactDamageAbAttr extends PostDefendAbAttr {
return false;
}
getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string {
override getTriggerMessage(pokemon: Pokemon, abilityName: string, ..._args: any[]): string {
return i18next.t("abilityTriggers:postDefendContactDamage", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
abilityName
@ -948,8 +954,8 @@ export class PostDefendPerishSongAbAttr extends PostDefendAbAttr {
this.turns = turns;
}
applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon)) {
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): boolean {
if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && !move.hitsSubstitute(attacker, pokemon)) {
if (pokemon.getTag(BattlerTagType.PERISH_SONG) || attacker.getTag(BattlerTagType.PERISH_SONG)) {
return false;
} else {
@ -963,24 +969,24 @@ export class PostDefendPerishSongAbAttr extends PostDefendAbAttr {
return false;
}
getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string {
override getTriggerMessage(pokemon: Pokemon, abilityName: string, ..._args: any[]): string {
return i18next.t("abilityTriggers:perishBody", { pokemonName: getPokemonNameWithAffix(pokemon), abilityName: abilityName });
}
}
export class PostDefendWeatherChangeAbAttr extends PostDefendAbAttr {
private weatherType: WeatherType;
protected condition: PokemonDefendCondition | null;
protected condition?: PokemonDefendCondition;
constructor(weatherType: WeatherType, condition?: PokemonDefendCondition) {
super();
this.weatherType = weatherType;
this.condition = condition ?? null;
this.condition = condition;
}
applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
if (this.condition !== null && !this.condition(pokemon, attacker, move)) {
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): boolean {
if (this.condition && !this.condition(pokemon, attacker, move) || move.hitsSubstitute(attacker, pokemon)) {
return false;
}
if (!pokemon.scene.arena.weather?.isImmutable()) {
@ -999,8 +1005,9 @@ export class PostDefendAbilitySwapAbAttr extends PostDefendAbAttr {
super();
}
applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && !attacker.getAbility().hasAttr(UnswappableAbilityAbAttr)) {
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, args: any[]): boolean {
if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon)
&& !attacker.getAbility().hasAttr(UnswappableAbilityAbAttr) && !move.hitsSubstitute(attacker, pokemon)) {
if (!simulated) {
const tempAbilityId = attacker.getAbility().id;
attacker.summonData.ability = pokemon.getAbility().id;
@ -1012,7 +1019,7 @@ export class PostDefendAbilitySwapAbAttr extends PostDefendAbAttr {
return false;
}
getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string {
override getTriggerMessage(pokemon: Pokemon, _abilityName: string, ..._args: any[]): string {
return i18next.t("abilityTriggers:postDefendAbilitySwap", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) });
}
}
@ -1025,8 +1032,9 @@ export class PostDefendAbilityGiveAbAttr extends PostDefendAbAttr {
this.ability = ability;
}
applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && !attacker.getAbility().hasAttr(UnsuppressableAbilityAbAttr) && !attacker.getAbility().hasAttr(PostDefendAbilityGiveAbAttr)) {
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): boolean {
if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && !attacker.getAbility().hasAttr(UnsuppressableAbilityAbAttr)
&& !attacker.getAbility().hasAttr(PostDefendAbilityGiveAbAttr) && !move.hitsSubstitute(attacker, pokemon)) {
if (!simulated) {
attacker.summonData.ability = this.ability;
}
@ -1037,7 +1045,7 @@ export class PostDefendAbilityGiveAbAttr extends PostDefendAbAttr {
return false;
}
getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string {
override getTriggerMessage(pokemon: Pokemon, abilityName: string, ..._args: any[]): string {
return i18next.t("abilityTriggers:postDefendAbilityGive", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
abilityName
@ -1056,8 +1064,8 @@ export class PostDefendMoveDisableAbAttr extends PostDefendAbAttr {
this.chance = chance;
}
applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
if (attacker.getTag(BattlerTagType.DISABLED) === null) {
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): boolean {
if (attacker.getTag(BattlerTagType.DISABLED) === null && !move.hitsSubstitute(attacker, pokemon)) {
if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && (this.chance === -1 || pokemon.randSeedInt(100) < this.chance)) {
if (simulated) {
return true;
@ -1724,17 +1732,17 @@ export class PostAttackApplyBattlerTagAbAttr extends PostAttackAbAttr {
}
export class PostDefendStealHeldItemAbAttr extends PostDefendAbAttr {
private condition: PokemonDefendCondition | null;
private condition?: PokemonDefendCondition;
constructor(condition?: PokemonDefendCondition) {
super();
this.condition = condition ?? null;
this.condition = condition;
}
applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): Promise<boolean> {
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, _args: any[]): Promise<boolean> {
return new Promise<boolean>(resolve => {
if (!simulated && hitResult < HitResult.NO_EFFECT && (!this.condition || this.condition(pokemon, attacker, move))) {
if (!simulated && hitResult < HitResult.NO_EFFECT && (!this.condition || this.condition(pokemon, attacker, move)) && !move.hitsSubstitute(attacker, pokemon)) {
const heldItems = this.getTargetHeldItems(attacker).filter(i => i.isTransferable);
if (heldItems.length) {
const stolenItem = heldItems[pokemon.randSeedInt(heldItems.length)];
@ -4476,7 +4484,7 @@ export class PostSummonStatStageChangeOnArenaAbAttr extends PostSummonStatStageC
export class FormBlockDamageAbAttr extends ReceivedMoveDamageMultiplierAbAttr {
private multiplier: number;
private tagType: BattlerTagType;
private recoilDamageFunc: ((pokemon: Pokemon) => number) | undefined;
private recoilDamageFunc?: ((pokemon: Pokemon) => number);
private triggerMessageFunc: (pokemon: Pokemon, abilityName: string) => string;
constructor(condition: PokemonDefendCondition, multiplier: number, tagType: BattlerTagType, triggerMessageFunc: (pokemon: Pokemon, abilityName: string) => string, recoilDamageFunc?: (pokemon: Pokemon) => number) {
@ -4492,16 +4500,16 @@ export class FormBlockDamageAbAttr extends ReceivedMoveDamageMultiplierAbAttr {
* Applies the pre-defense ability to the Pokémon.
* Removes the appropriate `BattlerTagType` when hit by an attack and is in its defense form.
*
* @param {Pokemon} pokemon The Pokémon with the ability.
* @param {boolean} passive n/a
* @param {Pokemon} attacker The attacking Pokémon.
* @param {PokemonMove} move The move being used.
* @param {Utils.BooleanHolder} cancelled n/a
* @param {any[]} args Additional arguments.
* @returns {boolean} Whether the immunity was applied.
* @param pokemon The Pokémon with the ability.
* @param _passive n/a
* @param attacker The attacking Pokémon.
* @param move The move being used.
* @param _cancelled n/a
* @param args Additional arguments.
* @returns `true` if the immunity was applied.
*/
applyPreDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean {
if (this.condition(pokemon, attacker, move)) {
override applyPreDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _cancelled: Utils.BooleanHolder, args: any[]): boolean {
if (this.condition(pokemon, attacker, move) && !move.hitsSubstitute(attacker, pokemon)) {
if (!simulated) {
(args[0] as Utils.NumberHolder).value = this.multiplier;
pokemon.removeTag(this.tagType);
@ -4517,12 +4525,12 @@ export class FormBlockDamageAbAttr extends ReceivedMoveDamageMultiplierAbAttr {
/**
* Gets the message triggered when the Pokémon avoids damage using the form-changing ability.
* @param {Pokemon} pokemon The Pokémon with the ability.
* @param {string} abilityName The name of the ability.
* @param {...any} args n/a
* @returns {string} The trigger message.
* @param pokemon The Pokémon with the ability.
* @param abilityName The name of the ability.
* @param _args n/a
* @returns The trigger message.
*/
getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string {
getTriggerMessage(pokemon: Pokemon, abilityName: string, ..._args: any[]): string {
return this.triggerMessageFunc(pokemon, abilityName);
}
}
@ -5503,7 +5511,8 @@ export function initAbilities() {
.attr(NoFusionAbilityAbAttr)
// Add BattlerTagType.DISGUISE if the pokemon is in its disguised form
.conditionalAttr(pokemon => pokemon.formIndex === 0, PostSummonAddBattlerTagAbAttr, BattlerTagType.DISGUISE, 0, false)
.attr(FormBlockDamageAbAttr, (target, user, move) => !!target.getTag(BattlerTagType.DISGUISE) && target.getMoveEffectiveness(user, move) > 0, 0, BattlerTagType.DISGUISE,
.attr(FormBlockDamageAbAttr,
(target, user, move) => !!target.getTag(BattlerTagType.DISGUISE) && target.getMoveEffectiveness(user, move) > 0, 0, BattlerTagType.DISGUISE,
(pokemon, abilityName) => i18next.t("abilityTriggers:disguiseAvoidedDamage", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName: abilityName }),
(pokemon) => Utils.toDmgValue(pokemon.getMaxHp() / 8))
.attr(PostBattleInitFormChangeAbAttr, () => 0)
@ -5665,7 +5674,8 @@ export function initAbilities() {
.conditionalAttr(getWeatherCondition(WeatherType.HAIL, WeatherType.SNOW), PostSummonAddBattlerTagAbAttr, BattlerTagType.ICE_FACE, 0)
// When weather changes to HAIL or SNOW while pokemon is fielded, add BattlerTagType.ICE_FACE
.attr(PostWeatherChangeAddBattlerTagAttr, BattlerTagType.ICE_FACE, 0, WeatherType.HAIL, WeatherType.SNOW)
.attr(FormBlockDamageAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL && !!target.getTag(BattlerTagType.ICE_FACE), 0, BattlerTagType.ICE_FACE,
.attr(FormBlockDamageAbAttr,
(target, user, move) => move.category === MoveCategory.PHYSICAL && !!target.getTag(BattlerTagType.ICE_FACE), 0, BattlerTagType.ICE_FACE,
(pokemon, abilityName) => i18next.t("abilityTriggers:iceFaceAvoidedDamage", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName: abilityName }))
.attr(PostBattleInitFormChangeAbAttr, () => 0)
.bypassFaint()

View File

@ -2139,6 +2139,10 @@ export class GulpMissileTag extends BattlerTag {
return false;
}
if (moveEffectPhase.move.getMove().hitsSubstitute(attacker, pokemon)) {
return true;
}
const cancelled = new Utils.BooleanHolder(false);
applyAbAttrs(BlockNonDirectDamageAbAttr, attacker, cancelled);

View File

@ -4844,14 +4844,14 @@ export class GulpMissileTagAttr extends MoveEffectAttr {
/**
* Adds BattlerTagType from GulpMissileTag based on the Pokemon's HP ratio.
* @param {Pokemon} user The Pokemon using the move.
* @param {Pokemon} target The Pokemon being targeted by the move.
* @param {Move} move The move being used.
* @param {any[]} args Additional arguments, if any.
* @param user The Pokemon using the move.
* @param _target N/A
* @param move The move being used.
* @param _args N/A
* @returns Whether the BattlerTag is applied.
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean | Promise<boolean> {
if (!super.apply(user, target, move, args)) {
apply(user: Pokemon, _target: Pokemon, move: Move, _args: any[]): boolean {
if (!super.apply(user, _target, move, _args)) {
return false;
}

View File

@ -1,8 +1,9 @@
import { BattlerIndex } from "#app/battle";
import { StatusEffect } from "#app/data/status-effect";
import { toDmgValue } from "#app/utils";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { StatusEffect } from "#app/data/status-effect";
import { Stat } from "#enums/stat";
import GameManager from "#test/utils/gameManager";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
@ -222,4 +223,17 @@ describe("Abilities - Disguise", () => {
expect(mimikyu.formIndex).toBe(bustedForm);
expect(mimikyu.hp).toBe(maxHp - disguiseDamage);
});
it("doesn't trigger if user is behind a substitute", async () => {
game.override
.enemyMoveset(Moves.SUBSTITUTE)
.moveset(Moves.POWER_TRIP);
await game.classicMode.startBattle();
game.move.select(Moves.POWER_TRIP);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.toNextTurn();
expect(game.scene.getEnemyPokemon()!.formIndex).toBe(disguisedForm);
});
});

View File

@ -1,13 +1,14 @@
import { BattlerTagType } from "#enums/battler-tag-type";
import { StatusEffect } from "#enums/status-effect";
import { BattlerIndex } from "#app/battle";
import Pokemon from "#app/field/pokemon";
import GameManager from "#test/utils/gameManager";
import { Abilities } from "#enums/abilities";
import { BattlerTagType } from "#enums/battler-tag-type";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { Stat } from "#enums/stat";
import { StatusEffect } from "#enums/status-effect";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { Stat } from "#enums/stat";
describe("Abilities - Gulp Missile", () => {
let phaserGame: Phaser.Game;
@ -40,8 +41,9 @@ describe("Abilities - Gulp Missile", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.disableCrits()
.battleType("single")
.moveset([ Moves.SURF, Moves.DIVE, Moves.SPLASH ])
.moveset([ Moves.SURF, Moves.DIVE, Moves.SPLASH, Moves.SUBSTITUTE ])
.enemySpecies(Species.SNORLAX)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH)
@ -234,6 +236,25 @@ describe("Abilities - Gulp Missile", () => {
expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.DEF)).toBe(-1);
});
it("doesn't trigger if user is behind a substitute", async () => {
game.override
.enemyAbility(Abilities.STURDY)
.enemyMoveset([ Moves.SPLASH, Moves.POWER_TRIP ]);
await game.classicMode.startBattle([ Species.CRAMORANT ]);
game.move.select(Moves.SURF);
await game.forceEnemyMove(Moves.SPLASH);
await game.toNextTurn();
expect(game.scene.getPlayerPokemon()!.formIndex).toBe(GULPING_FORM);
game.move.select(Moves.SUBSTITUTE);
await game.forceEnemyMove(Moves.POWER_TRIP);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.toNextTurn();
expect(game.scene.getPlayerPokemon()!.formIndex).toBe(GULPING_FORM);
});
it("cannot be suppressed", async () => {
game.override.enemyMoveset(Moves.GASTRO_ACID);

View File

@ -1,3 +1,4 @@
import { BattlerIndex } from "#app/battle";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { MoveEndPhase } from "#app/phases/move-end-phase";
import { QuietFormChangePhase } from "#app/phases/quiet-form-change-phase";
@ -36,7 +37,7 @@ describe("Abilities - Ice Face", () => {
});
it("takes no damage from physical move and transforms to Noice", async () => {
await game.startBattle([ Species.HITMONLEE ]);
await game.classicMode.startBattle([ Species.HITMONLEE ]);
game.move.select(Moves.TACKLE);
@ -52,7 +53,7 @@ describe("Abilities - Ice Face", () => {
it("takes no damage from the first hit of multihit physical move and transforms to Noice", async () => {
game.override.moveset([ Moves.SURGING_STRIKES ]);
game.override.enemyLevel(1);
await game.startBattle([ Species.HITMONLEE ]);
await game.classicMode.startBattle([ Species.HITMONLEE ]);
game.move.select(Moves.SURGING_STRIKES);
@ -78,7 +79,7 @@ describe("Abilities - Ice Face", () => {
});
it("takes damage from special moves", async () => {
await game.startBattle([ Species.MAGIKARP ]);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.move.select(Moves.ICE_BEAM);
@ -92,7 +93,7 @@ describe("Abilities - Ice Face", () => {
});
it("takes effects from status moves", async () => {
await game.startBattle([ Species.MAGIKARP ]);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.move.select(Moves.TOXIC_THREAD);
@ -108,7 +109,7 @@ describe("Abilities - Ice Face", () => {
game.override.moveset([ Moves.QUICK_ATTACK ]);
game.override.enemyMoveset([ Moves.HAIL, Moves.HAIL, Moves.HAIL, Moves.HAIL ]);
await game.startBattle([ Species.MAGIKARP ]);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.move.select(Moves.QUICK_ATTACK);
@ -130,7 +131,7 @@ describe("Abilities - Ice Face", () => {
game.override.enemyMoveset([ Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE ]);
game.override.moveset([ Moves.SNOWSCAPE ]);
await game.startBattle([ Species.EISCUE, Species.NINJASK ]);
await game.classicMode.startBattle([ Species.EISCUE, Species.NINJASK ]);
game.move.select(Moves.SNOWSCAPE);
@ -157,7 +158,7 @@ describe("Abilities - Ice Face", () => {
game.override.enemySpecies(Species.SHUCKLE);
game.override.enemyMoveset([ Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE ]);
await game.startBattle([ Species.EISCUE ]);
await game.classicMode.startBattle([ Species.EISCUE ]);
game.move.select(Moves.HAIL);
const eiscue = game.scene.getPlayerPokemon()!;
@ -176,7 +177,7 @@ describe("Abilities - Ice Face", () => {
it("persists form change when switched out", async () => {
game.override.enemyMoveset([ Moves.QUICK_ATTACK, Moves.QUICK_ATTACK, Moves.QUICK_ATTACK, Moves.QUICK_ATTACK ]);
await game.startBattle([ Species.EISCUE, Species.MAGIKARP ]);
await game.classicMode.startBattle([ Species.EISCUE, Species.MAGIKARP ]);
game.move.select(Moves.ICE_BEAM);
@ -205,7 +206,7 @@ describe("Abilities - Ice Face", () => {
[Species.EISCUE]: noiceForm,
});
await game.startBattle([ Species.EISCUE ]);
await game.classicMode.startBattle([ Species.EISCUE ]);
const eiscue = game.scene.getPlayerPokemon()!;
@ -222,10 +223,23 @@ describe("Abilities - Ice Face", () => {
expect(eiscue.getTag(BattlerTagType.ICE_FACE)).not.toBe(undefined);
});
it("doesn't trigger if user is behind a substitute", async () => {
game.override
.enemyMoveset(Moves.SUBSTITUTE)
.moveset(Moves.POWER_TRIP);
await game.classicMode.startBattle();
game.move.select(Moves.POWER_TRIP);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.toNextTurn();
expect(game.scene.getEnemyPokemon()!.formIndex).toBe(icefaceForm);
});
it("cannot be suppressed", async () => {
game.override.moveset([ Moves.GASTRO_ACID ]);
await game.startBattle([ Species.MAGIKARP ]);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.move.select(Moves.GASTRO_ACID);
@ -241,7 +255,7 @@ describe("Abilities - Ice Face", () => {
it("cannot be swapped with another ability", async () => {
game.override.moveset([ Moves.SKILL_SWAP ]);
await game.startBattle([ Species.MAGIKARP ]);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.move.select(Moves.SKILL_SWAP);
@ -257,7 +271,7 @@ describe("Abilities - Ice Face", () => {
it("cannot be copied", async () => {
game.override.ability(Abilities.TRACE);
await game.startBattle([ Species.MAGIKARP ]);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.move.select(Moves.SIMPLE_BEAM);