[Ability] Implement Unseen Fist (#1776)
* Implement Unseen Fist * Add unit tests for Unseen Fist * Fix unit test imports
This commit is contained in:
parent
17b103c447
commit
01435ed0e0
|
@ -3413,6 +3413,9 @@ export class SuppressFieldAbilitiesAbAttr extends AbAttr {
|
||||||
|
|
||||||
export class AlwaysHitAbAttr extends AbAttr { }
|
export class AlwaysHitAbAttr extends AbAttr { }
|
||||||
|
|
||||||
|
/** Attribute for abilities that allow moves that make contact to ignore protection (i.e. Unseen Fist) */
|
||||||
|
export class IgnoreProtectOnContactAbAttr extends AbAttr { }
|
||||||
|
|
||||||
export class UncopiableAbilityAbAttr extends AbAttr {
|
export class UncopiableAbilityAbAttr extends AbAttr {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(false);
|
super(false);
|
||||||
|
@ -4672,7 +4675,7 @@ export function initAbilities() {
|
||||||
new Ability(Abilities.QUICK_DRAW, 8)
|
new Ability(Abilities.QUICK_DRAW, 8)
|
||||||
.unimplemented(),
|
.unimplemented(),
|
||||||
new Ability(Abilities.UNSEEN_FIST, 8)
|
new Ability(Abilities.UNSEEN_FIST, 8)
|
||||||
.unimplemented(),
|
.attr(IgnoreProtectOnContactAbAttr),
|
||||||
new Ability(Abilities.CURIOUS_MEDICINE, 8)
|
new Ability(Abilities.CURIOUS_MEDICINE, 8)
|
||||||
.attr(PostSummonClearAllyStatsAbAttr),
|
.attr(PostSummonClearAllyStatsAbAttr),
|
||||||
new Ability(Abilities.TRANSISTOR, 8)
|
new Ability(Abilities.TRANSISTOR, 8)
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { Type } from "./type";
|
||||||
import * as Utils from "../utils";
|
import * as Utils from "../utils";
|
||||||
import { WeatherType } from "./weather";
|
import { WeatherType } from "./weather";
|
||||||
import { ArenaTagSide, ArenaTrapTag } from "./arena-tag";
|
import { ArenaTagSide, ArenaTrapTag } from "./arena-tag";
|
||||||
import { UnswappableAbilityAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, BlockRecoilDamageAttr, BlockOneHitKOAbAttr, IgnoreContactAbAttr, MaxMultiHitAbAttr, applyAbAttrs, BlockNonDirectDamageAbAttr, applyPreSwitchOutAbAttrs, PreSwitchOutAbAttr, applyPostDefendAbAttrs, PostDefendContactApplyStatusEffectAbAttr, MoveAbilityBypassAbAttr, ReverseDrainAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, BlockItemTheftAbAttr, applyPostAttackAbAttrs, ConfusionOnStatusEffectAbAttr, HealFromBerryUseAbAttr } from "./ability";
|
import { UnswappableAbilityAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, BlockRecoilDamageAttr, BlockOneHitKOAbAttr, IgnoreContactAbAttr, MaxMultiHitAbAttr, applyAbAttrs, BlockNonDirectDamageAbAttr, applyPreSwitchOutAbAttrs, PreSwitchOutAbAttr, applyPostDefendAbAttrs, PostDefendContactApplyStatusEffectAbAttr, MoveAbilityBypassAbAttr, ReverseDrainAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, BlockItemTheftAbAttr, applyPostAttackAbAttrs, ConfusionOnStatusEffectAbAttr, HealFromBerryUseAbAttr, IgnoreProtectOnContactAbAttr } from "./ability";
|
||||||
import { allAbilities } from "./ability";
|
import { allAbilities } from "./ability";
|
||||||
import { PokemonHeldItemModifier, BerryModifier, PreserveBerryModifier } from "../modifier/modifier";
|
import { PokemonHeldItemModifier, BerryModifier, PreserveBerryModifier } from "../modifier/modifier";
|
||||||
import { BattlerIndex } from "../battle";
|
import { BattlerIndex } from "../battle";
|
||||||
|
@ -559,6 +559,11 @@ export default class Move implements Localizable {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case MoveFlags.IGNORE_PROTECT:
|
||||||
|
if (user.hasAbilityWithAttr(IgnoreProtectOnContactAbAttr) &&
|
||||||
|
this.checkFlag(MoveFlags.MAKES_CONTACT, user, target)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return !!(this.flags & flag);
|
return !!(this.flags & flag);
|
||||||
|
@ -811,7 +816,8 @@ export class MoveEffectAttr extends MoveAttr {
|
||||||
*/
|
*/
|
||||||
canApply(user: Pokemon, target: Pokemon, move: Move, args: any[]) {
|
canApply(user: Pokemon, target: Pokemon, move: Move, args: any[]) {
|
||||||
return !! (this.selfTarget ? user.hp && !user.getTag(BattlerTagType.FRENZY) : target.hp)
|
return !! (this.selfTarget ? user.hp && !user.getTag(BattlerTagType.FRENZY) : target.hp)
|
||||||
&& (this.selfTarget || !target.getTag(BattlerTagType.PROTECTED) || move.hasFlag(MoveFlags.IGNORE_PROTECT));
|
&& (this.selfTarget || !target.getTag(BattlerTagType.PROTECTED) ||
|
||||||
|
move.checkFlag(MoveFlags.IGNORE_PROTECT, user, target));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Applies move effects so long as they are able based on {@linkcode canApply} */
|
/** Applies move effects so long as they are able based on {@linkcode canApply} */
|
||||||
|
|
|
@ -1729,7 +1729,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply arena tags for conditional protection
|
// Apply arena tags for conditional protection
|
||||||
if (!move.hasFlag(MoveFlags.IGNORE_PROTECT) && !move.isAllyTarget()) {
|
if (!move.checkFlag(MoveFlags.IGNORE_PROTECT, source, this) && !move.isAllyTarget()) {
|
||||||
const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||||
this.scene.arena.applyTagsForSide(ArenaTagType.QUICK_GUARD, defendingSide, cancelled, this, move.priority);
|
this.scene.arena.applyTagsForSide(ArenaTagType.QUICK_GUARD, defendingSide, cancelled, this, move.priority);
|
||||||
this.scene.arena.applyTagsForSide(ArenaTagType.WIDE_GUARD, defendingSide, cancelled, this, move.moveTarget);
|
this.scene.arena.applyTagsForSide(ArenaTagType.WIDE_GUARD, defendingSide, cancelled, this, move.moveTarget);
|
||||||
|
|
|
@ -2889,7 +2889,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isProtected = !move.hasFlag(MoveFlags.IGNORE_PROTECT) && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType));
|
const isProtected = !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target) && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType));
|
||||||
|
|
||||||
const firstHit = moveHistoryEntry.result !== MoveResult.SUCCESS;
|
const firstHit = moveHistoryEntry.result !== MoveResult.SUCCESS;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
|
import GameManager from "../utils/gameManager";
|
||||||
|
import * as Overrides from "#app/overrides";
|
||||||
|
import { Species } from "#enums/species";
|
||||||
|
import { Abilities } from "#enums/abilities";
|
||||||
|
import { Moves } from "#enums/moves";
|
||||||
|
import { getMovePosition } from "../utils/gameManagerUtils";
|
||||||
|
import { TurnEndPhase } from "#app/phases.js";
|
||||||
|
|
||||||
|
const TIMEOUT = 20 * 1000;
|
||||||
|
|
||||||
|
describe("Abilities - Unseen Fist", () => {
|
||||||
|
let phaserGame: Phaser.Game;
|
||||||
|
let game: GameManager;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
vi.spyOn(Overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
|
||||||
|
vi.spyOn(Overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.URSHIFU);
|
||||||
|
vi.spyOn(Overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.SNORLAX);
|
||||||
|
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.PROTECT, Moves.PROTECT, Moves.PROTECT, Moves.PROTECT]);
|
||||||
|
vi.spyOn(Overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability causes a contact move to ignore Protect",
|
||||||
|
() => testUnseenFistHitResult(game, Moves.QUICK_ATTACK, Moves.PROTECT, true),
|
||||||
|
TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability does not cause a non-contact move to ignore Protect",
|
||||||
|
() => testUnseenFistHitResult(game, Moves.ABSORB, Moves.PROTECT, false),
|
||||||
|
TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability does not apply if the source has Long Reach",
|
||||||
|
() => {
|
||||||
|
vi.spyOn(Overrides, "PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.LONG_REACH);
|
||||||
|
testUnseenFistHitResult(game, Moves.QUICK_ATTACK, Moves.PROTECT, false);
|
||||||
|
}, TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability causes a contact move to ignore Wide Guard",
|
||||||
|
() => testUnseenFistHitResult(game, Moves.BREAKING_SWIPE, Moves.WIDE_GUARD, true),
|
||||||
|
TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ability does not cause a non-contact move to ignore Wide Guard",
|
||||||
|
() => testUnseenFistHitResult(game, Moves.BULLDOZE, Moves.WIDE_GUARD, false),
|
||||||
|
TIMEOUT
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function testUnseenFistHitResult(game: GameManager, attackMove: Moves, protectMove: Moves, shouldSucceed: boolean = true): Promise<void> {
|
||||||
|
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([attackMove]);
|
||||||
|
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([protectMove, protectMove, protectMove, protectMove]);
|
||||||
|
|
||||||
|
await game.startBattle();
|
||||||
|
|
||||||
|
const leadPokemon = game.scene.getPlayerPokemon();
|
||||||
|
expect(leadPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||||
|
expect(enemyPokemon).not.toBe(undefined);
|
||||||
|
|
||||||
|
const enemyStartingHp = enemyPokemon.hp;
|
||||||
|
|
||||||
|
game.doAttack(getMovePosition(game.scene, 0, attackMove));
|
||||||
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
|
if (shouldSucceed) {
|
||||||
|
expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp);
|
||||||
|
} else {
|
||||||
|
expect(enemyPokemon.hp).toBe(enemyStartingHp);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue