Skip to content

Commit

Permalink
[Ability] Implement Unseen Fist (pagefaultgames#1776)
Browse files Browse the repository at this point in the history
* Implement Unseen Fist

* Add unit tests for Unseen Fist

* Fix unit test imports
  • Loading branch information
innerthunder authored Jun 16, 2024
1 parent 17b103c commit 01435ed
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 5 deletions.
5 changes: 4 additions & 1 deletion src/data/ability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3413,6 +3413,9 @@ export class SuppressFieldAbilitiesAbAttr 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 {
constructor() {
super(false);
Expand Down Expand Up @@ -4672,7 +4675,7 @@ export function initAbilities() {
new Ability(Abilities.QUICK_DRAW, 8)
.unimplemented(),
new Ability(Abilities.UNSEEN_FIST, 8)
.unimplemented(),
.attr(IgnoreProtectOnContactAbAttr),
new Ability(Abilities.CURIOUS_MEDICINE, 8)
.attr(PostSummonClearAllyStatsAbAttr),
new Ability(Abilities.TRANSISTOR, 8)
Expand Down
10 changes: 8 additions & 2 deletions src/data/move.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Type } from "./type";
import * as Utils from "../utils";
import { WeatherType } from "./weather";
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 { PokemonHeldItemModifier, BerryModifier, PreserveBerryModifier } from "../modifier/modifier";
import { BattlerIndex } from "../battle";
Expand Down Expand Up @@ -559,6 +559,11 @@ export default class Move implements Localizable {
return true;
}
}
case MoveFlags.IGNORE_PROTECT:
if (user.hasAbilityWithAttr(IgnoreProtectOnContactAbAttr) &&
this.checkFlag(MoveFlags.MAKES_CONTACT, user, target)) {
return true;
}
}

return !!(this.flags & flag);
Expand Down Expand Up @@ -811,7 +816,8 @@ export class MoveEffectAttr extends MoveAttr {
*/
canApply(user: Pokemon, target: Pokemon, move: Move, args: any[]) {
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} */
Expand Down
2 changes: 1 addition & 1 deletion src/field/pokemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1729,7 +1729,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}

// 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;
this.scene.arena.applyTagsForSide(ArenaTagType.QUICK_GUARD, defendingSide, cancelled, this, move.priority);
this.scene.arena.applyTagsForSide(ArenaTagType.WIDE_GUARD, defendingSide, cancelled, this, move.moveTarget);
Expand Down
2 changes: 1 addition & 1 deletion src/phases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2889,7 +2889,7 @@ export class MoveEffectPhase extends PokemonPhase {
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;

Expand Down
91 changes: 91 additions & 0 deletions src/test/abilities/unseen_fist.test.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}

0 comments on commit 01435ed

Please sign in to comment.