Skip to content

Commit

Permalink
[Bug] Flash Fire/etc now trigger even if the attack would miss (#4337)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
PrabbyDD and DayKev committed Sep 23, 2024
1 parent 4557a73 commit 3d4eadb
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 11 deletions.
4 changes: 4 additions & 0 deletions src/data/ability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,10 @@ export class TypeImmunityAbAttr extends PreDefendAbAttr {
return false;
}

getImmuneType(): Type | null {
return this.immuneType;
}

override getCondition(): AbAttrCondition | null {
return this.condition;
}
Expand Down
16 changes: 12 additions & 4 deletions src/phases/move-effect-phase.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import BattleScene from "#app/battle-scene";
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 { MoveAnim } from "#app/data/battle-anims";
import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag } from "#app/data/battler-tags";
Expand Down Expand Up @@ -97,12 +97,17 @@ export class MoveEffectPhase extends PokemonPhase {
*/
const targetHitChecks = Object.fromEntries(targets.map(p => [p.getBattlerIndex(), this.hitCheck(p)]));
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
* (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.
*/
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();
if (hasActiveTargets) {
this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getTarget()? getPokemonNameWithAffix(this.getTarget()!) : "" }));
Expand Down Expand Up @@ -132,7 +137,7 @@ export class MoveEffectPhase extends PokemonPhase {
const hasConditionalProtectApplied = new Utils.BooleanHolder(false);
/** Does the applied conditional protection bypass Protect-ignoring effects? */
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()) {
this.scene.arena.applyTagsForSide(ConditionalProtectTag, targetSide, hasConditionalProtectApplied, user, target, move.id, bypassIgnoreProtect);
}
Expand All @@ -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)))
|| (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
* 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.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(target) }));
if (moveHistoryEntry.result === MoveResult.PENDING) {
Expand Down
14 changes: 14 additions & 0 deletions src/test/abilities/dry_skin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,18 @@ describe("Abilities - Dry Skin", () => {

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());
});
});
41 changes: 35 additions & 6 deletions src/test/abilities/flash_fire.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe("Abilities - Flash Fire", () => {

it("immune to Fire-type moves", async () => {
game.override.enemyMoveset([Moves.EMBER]).moveset(Moves.SPLASH);
await game.startBattle([Species.BLISSEY]);
await game.classicMode.startBattle([Species.BLISSEY]);

const blissey = game.scene.getPlayerPokemon()!;

Expand All @@ -49,7 +49,7 @@ describe("Abilities - Flash Fire", () => {

it("not activate if the Pokémon is protected from the Fire-type move", async () => {
game.override.enemyMoveset([Moves.EMBER]).moveset([Moves.PROTECT]);
await game.startBattle([Species.BLISSEY]);
await game.classicMode.startBattle([Species.BLISSEY]);

const blissey = game.scene.getPlayerPokemon()!;

Expand All @@ -60,7 +60,7 @@ describe("Abilities - Flash Fire", () => {

it("activated by Will-O-Wisp", async () => {
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()!;

Expand All @@ -76,7 +76,7 @@ describe("Abilities - Flash Fire", () => {
it("activated after being frozen", async () => {
game.override.enemyMoveset([Moves.EMBER]).moveset(Moves.SPLASH);
game.override.statusEffect(StatusEffect.FREEZE);
await game.startBattle([Species.BLISSEY]);
await game.classicMode.startBattle([Species.BLISSEY]);

const blissey = game.scene.getPlayerPokemon()!;

Expand All @@ -88,7 +88,7 @@ describe("Abilities - Flash Fire", () => {

it("not passing with baton pass", async () => {
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
game.move.select(Moves.BATON_PASS);
Expand All @@ -105,7 +105,7 @@ describe("Abilities - Flash Fire", () => {
it("boosts Fire-type move when the ability is activated", async () => {
game.override.enemyMoveset([Moves.FIRE_PLEDGE]).moveset([Moves.EMBER, Moves.SPLASH]);
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 initialHP = 1000;
blissey.hp = initialHP;
Expand All @@ -126,4 +126,33 @@ describe("Abilities - Flash Fire", () => {

expect(flashFireDmg).toBeGreaterThan(originalDmg);
}, 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);
});
18 changes: 18 additions & 0 deletions src/test/abilities/sap_sipper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,22 @@ describe("Abilities - Sap Sipper", () => {
expect(initialEnemyHp - enemyPokemon.hp).toBe(0);
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);
});
});
22 changes: 21 additions & 1 deletion src/test/abilities/volt_absorb.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { BattlerIndex } from "#app/battle";

// See also: TypeImmunityAbAttr
describe("Abilities - Volt Absorb", () => {
Expand Down Expand Up @@ -39,7 +40,7 @@ describe("Abilities - Volt Absorb", () => {
game.override.enemySpecies(Species.DUSKULL);
game.override.enemyAbility(Abilities.BALL_FETCH);

await game.startBattle();
await game.classicMode.startBattle();

const playerPokemon = game.scene.getPlayerPokemon()!;

Expand All @@ -51,4 +52,23 @@ describe("Abilities - Volt Absorb", () => {
expect(playerPokemon.getTag(BattlerTagType.CHARGED)).toBeDefined();
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());
});
});

0 comments on commit 3d4eadb

Please sign in to comment.