[Bug] Flash Fire/etc now trigger even if the attack would miss (#4337)

* adding immunity check

* making tests

* modifying and adding tests

* making tests more rigorous

* changing hitcheck return to be what it was originally, no significant effect

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
This commit is contained in:
PrabbyDD 2024-09-23 12:37:21 -07:00 committed by GitHub
parent 4557a73ecc
commit 3d4eadbc3e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 104 additions and 11 deletions

View File

@ -366,6 +366,10 @@ export class TypeImmunityAbAttr extends PreDefendAbAttr {
return false; return false;
} }
getImmuneType(): Type | null {
return this.immuneType;
}
override getCondition(): AbAttrCondition | null { override getCondition(): AbAttrCondition | null {
return this.condition; return this.condition;
} }

View File

@ -1,6 +1,6 @@
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { applyPreAttackAbAttrs, AddSecondStrikeAbAttr, IgnoreMoveEffectsAbAttr, applyPostDefendAbAttrs, PostDefendAbAttr, applyPostAttackAbAttrs, PostAttackAbAttr, MaxMultiHitAbAttr, AlwaysHitAbAttr } from "#app/data/ability"; import { applyPreAttackAbAttrs, AddSecondStrikeAbAttr, IgnoreMoveEffectsAbAttr, applyPostDefendAbAttrs, PostDefendAbAttr, applyPostAttackAbAttrs, PostAttackAbAttr, MaxMultiHitAbAttr, AlwaysHitAbAttr, TypeImmunityAbAttr } from "#app/data/ability";
import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag"; import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag";
import { MoveAnim } from "#app/data/battle-anims"; import { MoveAnim } from "#app/data/battle-anims";
import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag } from "#app/data/battler-tags"; import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag } from "#app/data/battler-tags";
@ -97,12 +97,17 @@ export class MoveEffectPhase extends PokemonPhase {
*/ */
const targetHitChecks = Object.fromEntries(targets.map(p => [p.getBattlerIndex(), this.hitCheck(p)])); const targetHitChecks = Object.fromEntries(targets.map(p => [p.getBattlerIndex(), this.hitCheck(p)]));
const hasActiveTargets = targets.some(t => t.isActive(true)); const hasActiveTargets = targets.some(t => t.isActive(true));
/** Check if the target is immune via ability to the attacking move */
const isImmune = targets[0].hasAbilityWithAttr(TypeImmunityAbAttr) && (targets[0].getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move));
/** /**
* If no targets are left for the move to hit (FAIL), or the invoked move is single-target * If no targets are left for the move to hit (FAIL), or the invoked move is single-target
* (and not random target) and failed the hit check against its target (MISS), log the move * (and not random target) and failed the hit check against its target (MISS), log the move
* as FAILed or MISSed (depending on the conditions above) and end this phase. * as FAILed or MISSed (depending on the conditions above) and end this phase.
*/ */
if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag))) {
if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) {
this.stopMultiHit(); this.stopMultiHit();
if (hasActiveTargets) { if (hasActiveTargets) {
this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getTarget()? getPokemonNameWithAffix(this.getTarget()!) : "" })); this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getTarget()? getPokemonNameWithAffix(this.getTarget()!) : "" }));
@ -132,7 +137,7 @@ export class MoveEffectPhase extends PokemonPhase {
const hasConditionalProtectApplied = new Utils.BooleanHolder(false); const hasConditionalProtectApplied = new Utils.BooleanHolder(false);
/** Does the applied conditional protection bypass Protect-ignoring effects? */ /** Does the applied conditional protection bypass Protect-ignoring effects? */
const bypassIgnoreProtect = new Utils.BooleanHolder(false); const bypassIgnoreProtect = new Utils.BooleanHolder(false);
// If the move is not targeting a Pokemon on the user's side, try to apply conditional protection effects /** If the move is not targeting a Pokemon on the user's side, try to apply conditional protection effects */
if (!this.move.getMove().isAllyTarget()) { if (!this.move.getMove().isAllyTarget()) {
this.scene.arena.applyTagsForSide(ConditionalProtectTag, targetSide, hasConditionalProtectApplied, user, target, move.id, bypassIgnoreProtect); this.scene.arena.applyTagsForSide(ConditionalProtectTag, targetSide, hasConditionalProtectApplied, user, target, move.id, bypassIgnoreProtect);
} }
@ -142,11 +147,14 @@ export class MoveEffectPhase extends PokemonPhase {
&& (hasConditionalProtectApplied.value || (!target.findTags(t => t instanceof DamageProtectedTag).length && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType))) && (hasConditionalProtectApplied.value || (!target.findTags(t => t instanceof DamageProtectedTag).length && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType)))
|| (this.move.getMove().category !== MoveCategory.STATUS && target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType)))); || (this.move.getMove().category !== MoveCategory.STATUS && target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType))));
/** Is the pokemon immune due to an ablility? */
const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr) && (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move));
/** /**
* If the move missed a target, stop all future hits against that target * If the move missed a target, stop all future hits against that target
* and move on to the next target (if there is one). * and move on to the next target (if there is one).
*/ */
if (!isProtected && !targetHitChecks[target.getBattlerIndex()]) { if (!isImmune && !isProtected && !targetHitChecks[target.getBattlerIndex()]) {
this.stopMultiHit(target); this.stopMultiHit(target);
this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(target) })); this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(target) }));
if (moveHistoryEntry.result === MoveResult.PENDING) { if (moveHistoryEntry.result === MoveResult.PENDING) {

View File

@ -141,4 +141,18 @@ describe("Abilities - Dry Skin", () => {
expect(healthGainedFromWaterShuriken).toBe(healthGainedFromWaterGun); expect(healthGainedFromWaterShuriken).toBe(healthGainedFromWaterGun);
}); });
it("opposing water moves still heal regardless of accuracy check", async () => {
await game.classicMode.startBattle();
const enemy = game.scene.getEnemyPokemon()!;
game.move.select(Moves.WATER_GUN);
enemy.hp = enemy.hp - 1;
await game.phaseInterceptor.to("MoveEffectPhase");
await game.move.forceMiss();
await game.phaseInterceptor.to("BerryPhase", false);
expect(enemy.hp).toBe(enemy.getMaxHp());
});
}); });

View File

@ -38,7 +38,7 @@ describe("Abilities - Flash Fire", () => {
it("immune to Fire-type moves", async () => { it("immune to Fire-type moves", async () => {
game.override.enemyMoveset([Moves.EMBER]).moveset(Moves.SPLASH); game.override.enemyMoveset([Moves.EMBER]).moveset(Moves.SPLASH);
await game.startBattle([Species.BLISSEY]); await game.classicMode.startBattle([Species.BLISSEY]);
const blissey = game.scene.getPlayerPokemon()!; const blissey = game.scene.getPlayerPokemon()!;
@ -49,7 +49,7 @@ describe("Abilities - Flash Fire", () => {
it("not activate if the Pokémon is protected from the Fire-type move", async () => { it("not activate if the Pokémon is protected from the Fire-type move", async () => {
game.override.enemyMoveset([Moves.EMBER]).moveset([Moves.PROTECT]); game.override.enemyMoveset([Moves.EMBER]).moveset([Moves.PROTECT]);
await game.startBattle([Species.BLISSEY]); await game.classicMode.startBattle([Species.BLISSEY]);
const blissey = game.scene.getPlayerPokemon()!; const blissey = game.scene.getPlayerPokemon()!;
@ -60,7 +60,7 @@ describe("Abilities - Flash Fire", () => {
it("activated by Will-O-Wisp", async () => { it("activated by Will-O-Wisp", async () => {
game.override.enemyMoveset([Moves.WILL_O_WISP]).moveset(Moves.SPLASH); game.override.enemyMoveset([Moves.WILL_O_WISP]).moveset(Moves.SPLASH);
await game.startBattle([Species.BLISSEY]); await game.classicMode.startBattle([Species.BLISSEY]);
const blissey = game.scene.getPlayerPokemon()!; const blissey = game.scene.getPlayerPokemon()!;
@ -76,7 +76,7 @@ describe("Abilities - Flash Fire", () => {
it("activated after being frozen", async () => { it("activated after being frozen", async () => {
game.override.enemyMoveset([Moves.EMBER]).moveset(Moves.SPLASH); game.override.enemyMoveset([Moves.EMBER]).moveset(Moves.SPLASH);
game.override.statusEffect(StatusEffect.FREEZE); game.override.statusEffect(StatusEffect.FREEZE);
await game.startBattle([Species.BLISSEY]); await game.classicMode.startBattle([Species.BLISSEY]);
const blissey = game.scene.getPlayerPokemon()!; const blissey = game.scene.getPlayerPokemon()!;
@ -88,7 +88,7 @@ describe("Abilities - Flash Fire", () => {
it("not passing with baton pass", async () => { it("not passing with baton pass", async () => {
game.override.enemyMoveset([Moves.EMBER]).moveset([Moves.BATON_PASS]); game.override.enemyMoveset([Moves.EMBER]).moveset([Moves.BATON_PASS]);
await game.startBattle([Species.BLISSEY, Species.CHANSEY]); await game.classicMode.startBattle([Species.BLISSEY, Species.CHANSEY]);
// ensure use baton pass after enemy moved // ensure use baton pass after enemy moved
game.move.select(Moves.BATON_PASS); game.move.select(Moves.BATON_PASS);
@ -105,7 +105,7 @@ describe("Abilities - Flash Fire", () => {
it("boosts Fire-type move when the ability is activated", async () => { it("boosts Fire-type move when the ability is activated", async () => {
game.override.enemyMoveset([Moves.FIRE_PLEDGE]).moveset([Moves.EMBER, Moves.SPLASH]); game.override.enemyMoveset([Moves.FIRE_PLEDGE]).moveset([Moves.EMBER, Moves.SPLASH]);
game.override.enemyAbility(Abilities.FLASH_FIRE).ability(Abilities.NONE); game.override.enemyAbility(Abilities.FLASH_FIRE).ability(Abilities.NONE);
await game.startBattle([Species.BLISSEY]); await game.classicMode.startBattle([Species.BLISSEY]);
const blissey = game.scene.getPlayerPokemon()!; const blissey = game.scene.getPlayerPokemon()!;
const initialHP = 1000; const initialHP = 1000;
blissey.hp = initialHP; blissey.hp = initialHP;
@ -126,4 +126,33 @@ describe("Abilities - Flash Fire", () => {
expect(flashFireDmg).toBeGreaterThan(originalDmg); expect(flashFireDmg).toBeGreaterThan(originalDmg);
}, 20000); }, 20000);
it("still activates regardless of accuracy check", async () => {
game.override.moveset(Moves.FIRE_PLEDGE).enemyMoveset(Moves.EMBER);
game.override.enemyAbility(Abilities.NONE).ability(Abilities.FLASH_FIRE);
game.override.enemySpecies(Species.BLISSEY);
await game.classicMode.startBattle([Species.RATTATA]);
const blissey = game.scene.getEnemyPokemon()!;
const initialHP = 1000;
blissey.hp = initialHP;
// first turn
game.move.select(Moves.FIRE_PLEDGE);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to("MoveEffectPhase");
await game.move.forceMiss();
await game.phaseInterceptor.to(TurnEndPhase);
const originalDmg = initialHP - blissey.hp;
expect(blissey.hp > 0);
blissey.hp = initialHP;
// second turn
game.move.select(Moves.FIRE_PLEDGE);
await game.phaseInterceptor.to(TurnEndPhase);
const flashFireDmg = initialHP - blissey.hp;
expect(flashFireDmg).toBeGreaterThan(originalDmg);
}, 20000);
}); });

View File

@ -165,4 +165,22 @@ describe("Abilities - Sap Sipper", () => {
expect(initialEnemyHp - enemyPokemon.hp).toBe(0); expect(initialEnemyHp - enemyPokemon.hp).toBe(0);
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1); expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
}); });
it("still activates regardless of accuracy check", async () => {
game.override.moveset(Moves.LEAF_BLADE);
game.override.enemyMoveset(Moves.SPLASH);
game.override.enemySpecies(Species.MAGIKARP);
game.override.enemyAbility(Abilities.SAP_SIPPER);
await game.classicMode.startBattle();
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.LEAF_BLADE);
await game.phaseInterceptor.to("MoveEffectPhase");
await game.move.forceMiss();
await game.phaseInterceptor.to("BerryPhase", false);
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
});
}); });

View File

@ -7,6 +7,7 @@ import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { BattlerIndex } from "#app/battle";
// See also: TypeImmunityAbAttr // See also: TypeImmunityAbAttr
describe("Abilities - Volt Absorb", () => { describe("Abilities - Volt Absorb", () => {
@ -39,7 +40,7 @@ describe("Abilities - Volt Absorb", () => {
game.override.enemySpecies(Species.DUSKULL); game.override.enemySpecies(Species.DUSKULL);
game.override.enemyAbility(Abilities.BALL_FETCH); game.override.enemyAbility(Abilities.BALL_FETCH);
await game.startBattle(); await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!; const playerPokemon = game.scene.getPlayerPokemon()!;
@ -51,4 +52,23 @@ describe("Abilities - Volt Absorb", () => {
expect(playerPokemon.getTag(BattlerTagType.CHARGED)).toBeDefined(); expect(playerPokemon.getTag(BattlerTagType.CHARGED)).toBeDefined();
expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase");
}); });
it("should activate regardless of accuracy checks", async () => {
game.override.moveset(Moves.THUNDERBOLT);
game.override.enemyMoveset(Moves.SPLASH);
game.override.enemySpecies(Species.MAGIKARP);
game.override.enemyAbility(Abilities.VOLT_ABSORB);
await game.classicMode.startBattle();
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.THUNDERBOLT);
enemyPokemon.hp = enemyPokemon.hp - 1;
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("MoveEffectPhase");
await game.move.forceMiss();
await game.phaseInterceptor.to("BerryPhase", false);
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
});
}); });