[Ability] Implement Unseen Fist (#1776)

* Implement Unseen Fist

* Add unit tests for Unseen Fist

* Fix unit test imports
This commit is contained in:
innerthunder 2024-06-15 19:14:49 -07:00 committed by GitHub
parent 17b103c447
commit 01435ed0e0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 105 additions and 5 deletions

View File

@ -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)

View File

@ -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} */

View File

@ -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);

View File

@ -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;

View File

@ -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);
}
}