[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
|
* (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]])) {
|
if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag))) {
|
||||||
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()!) : "" }));
|
||||||
|
@ -125,20 +125,6 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||||
/** Has the move successfully hit a target (for damage) yet? */
|
/** Has the move successfully hit a target (for damage) yet? */
|
||||||
let hasHit: boolean = false;
|
let hasHit: boolean = false;
|
||||||
for (const target of targets) {
|
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 */
|
/** The {@linkcode ArenaTagSide} to which the target belongs */
|
||||||
const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
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)))
|
&& (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))));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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? */
|
/** Does this phase represent the invoked move's first strike? */
|
||||||
const firstHit = (user.turnData.hitsLeft === user.turnData.hitCount);
|
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);
|
expect(enemy.getStatStage(Stat.DEF)).toBe(-2);
|
||||||
}, TIMEOUT);
|
}, 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 () => {
|
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));
|
game.override.enemyMoveset(Array(4).fill(Moves.WATER_GUN));
|
||||||
await game.classicMode.startBattle();
|
await game.classicMode.startBattle();
|
||||||
|
|
Loading…
Reference in New Issue