[Bug] Fix for "Moves Can Miss Against Protect, Baneful Bunker, King's Shield" (#4262)
* added various tests for protect based moves, reset protect test file bc no easy way to test specifically with protect, and changes in move-effect to fix the issue * adding another non contact move test for baneful bunker * Update src/test/moves/obstruct.test.ts Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> * Update src/test/moves/baneful_bunker.test.ts Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> * Update src/test/moves/baneful_bunker.test.ts Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> * Update src/test/moves/baneful_bunker.test.ts Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> * better descriptions for baneful bunker test --------- Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com>
This commit is contained in:
parent
79fa80cfd8
commit
3a683c0663
|
@ -102,7 +102,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||
* (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]])) {
|
||||
if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag))) {
|
||||
this.stopMultiHit();
|
||||
if (hasActiveTargets) {
|
||||
this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getTarget()? getPokemonNameWithAffix(this.getTarget()!) : "" }));
|
||||
|
@ -125,20 +125,6 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||
/** Has the move successfully hit a target (for damage) yet? */
|
||||
let hasHit: boolean = false;
|
||||
for (const target of targets) {
|
||||
/**
|
||||
* 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 (!targetHitChecks[target.getBattlerIndex()]) {
|
||||
this.stopMultiHit(target);
|
||||
this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(target) }));
|
||||
if (moveHistoryEntry.result === MoveResult.PENDING) {
|
||||
moveHistoryEntry.result = MoveResult.MISS;
|
||||
}
|
||||
user.pushMoveHistory(moveHistoryEntry);
|
||||
applyMoveAttrs(MissEffectAttr, user, null, move);
|
||||
continue;
|
||||
}
|
||||
|
||||
/** The {@linkcode ArenaTagSide} to which the target belongs */
|
||||
const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||
|
@ -156,6 +142,21 @@ 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))));
|
||||
|
||||
/**
|
||||
* 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()]) {
|
||||
this.stopMultiHit(target);
|
||||
this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(target) }));
|
||||
if (moveHistoryEntry.result === MoveResult.PENDING) {
|
||||
moveHistoryEntry.result = MoveResult.MISS;
|
||||
}
|
||||
user.pushMoveHistory(moveHistoryEntry);
|
||||
applyMoveAttrs(MissEffectAttr, user, null, move);
|
||||
continue;
|
||||
}
|
||||
|
||||
/** Does this phase represent the invoked move's first strike? */
|
||||
const firstHit = (user.turnData.hitsLeft === user.turnData.hitCount);
|
||||
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
||||
import GameManager from "../utils/gameManager";
|
||||
import { Species } from "#enums/species";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { StatusEffect } from "#app/enums/status-effect";
|
||||
|
||||
const TIMEOUT = 20 * 1000;
|
||||
|
||||
describe("Moves - Baneful Bunker", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
|
||||
game.override.battleType("single");
|
||||
|
||||
game.override.moveset(Moves.SLASH);
|
||||
|
||||
game.override.enemySpecies(Species.SNORLAX);
|
||||
game.override.enemyAbility(Abilities.INSOMNIA);
|
||||
game.override.enemyMoveset(Moves.BANEFUL_BUNKER);
|
||||
|
||||
game.override.startingLevel(100);
|
||||
game.override.enemyLevel(100);
|
||||
});
|
||||
test(
|
||||
"should protect the user and poison attackers that make contact",
|
||||
async () => {
|
||||
await game.classicMode.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.SLASH);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
||||
expect(leadPokemon.status?.effect === StatusEffect.POISON).toBeTruthy();
|
||||
}, TIMEOUT
|
||||
);
|
||||
test(
|
||||
"should protect the user and poison attackers that make contact, regardless of accuracy checks",
|
||||
async () => {
|
||||
await game.classicMode.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.SLASH);
|
||||
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());
|
||||
expect(leadPokemon.status?.effect === StatusEffect.POISON).toBeTruthy();
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"should not poison attackers that don't make contact",
|
||||
async () => {
|
||||
game.override.moveset(Moves.FLASH_CANNON);
|
||||
await game.classicMode.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.FLASH_CANNON);
|
||||
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());
|
||||
expect(leadPokemon.status?.effect === StatusEffect.POISON).toBeFalsy();
|
||||
}, TIMEOUT
|
||||
);
|
||||
});
|
|
@ -43,6 +43,23 @@ describe("Moves - Obstruct", () => {
|
|||
expect(enemy.getStatStage(Stat.DEF)).toBe(-2);
|
||||
}, TIMEOUT);
|
||||
|
||||
it("bypasses accuracy checks when applying protection and defense reduction", async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.ICE_PUNCH));
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
game.move.select(Moves.OBSTRUCT);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
await game.move.forceMiss();
|
||||
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(player.isFullHp()).toBe(true);
|
||||
expect(enemy.getStatStage(Stat.DEF)).toBe(-2);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it("protects from non-contact damaging moves and doesn't lower the opponent's defense by 2 stages", async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.WATER_GUN));
|
||||
await game.classicMode.startBattle();
|
||||
|
|
Loading…
Reference in New Issue