Updated a few telekinesis tests to not brok

This commit is contained in:
Bertie690 2025-09-07 13:02:08 -04:00
parent 5eed7f7e01
commit c524cbd053
8 changed files with 120 additions and 52 deletions

View File

@ -115,7 +115,17 @@ export class BattlerTag implements BaseBattlerTag {
//#region non-serializable fields //#region non-serializable fields
// Fields that should never be serialized, as they must not change after instantiation // Fields that should never be serialized, as they must not change after instantiation
/**
* Whether this Tag can be transferred via {@linkcode MoveId.BATON_PASS}.
* @defaultValue `false`
* @todo Make this an overriddable getter on subclasses rather than a value defined in the constructor
*/
#isBatonPassable = false; #isBatonPassable = false;
/**
* Whether this Tag can be transferred via {@linkcode MoveId.BATON_PASS}.
* @defaultValue `false`
*/
public get isBatonPassable(): boolean { public get isBatonPassable(): boolean {
return this.#isBatonPassable; return this.#isBatonPassable;
} }
@ -2239,7 +2249,7 @@ export abstract class TypeImmuneTag extends SerializableBattlerTag {
} }
/** /**
* Battler Tag that lifts the affected Pokemon into the air and provides immunity to Ground type moves. * Battler Tag that lifts the affected Pokemon into the air, providing immunity to Ground-type moves.
* @see {@link https://bulbapedia.bulbagarden.net/wiki/Magnet_Rise_(move) | MoveId.MAGNET_RISE} * @see {@link https://bulbapedia.bulbagarden.net/wiki/Magnet_Rise_(move) | MoveId.MAGNET_RISE}
* @see {@link https://bulbapedia.bulbagarden.net/wiki/Telekinesis_(move) | MoveId.TELEKINESIS} * @see {@link https://bulbapedia.bulbagarden.net/wiki/Telekinesis_(move) | MoveId.TELEKINESIS}
*/ */
@ -3470,22 +3480,15 @@ export class SyrupBombTag extends SerializableBattlerTag {
} }
/** /**
* Telekinesis raises the target into the air for three turns and causes all moves used against the target (aside from OHKO moves) to hit the target unless the target is in a semi-invulnerable state from Fly/Dig. * Tag used by {@linkcode MoveId.TELEKINESIS} to provide its guaranteed-hit effect. \
* The first effect is provided by {@linkcode FloatingTag}, the accuracy-bypass effect is provided by TelekinesisTag * The effects of Telekinesis can be Baton Passed to a teammate, including ones unaffected by the original move.
* The effects of Telekinesis can be baton passed to a teammate. * A notable exception is Mega Gengar, which cannot receive either effect via Baton Pass.
* @see {@link https://bulbapedia.bulbagarden.net/wiki/Telekinesis_(move) | MoveId.TELEKINESIS} * @see {@linkcode FloatingTag} - Tag used by Telekinesis to unground the target
*/ */
export class TelekinesisTag extends SerializableBattlerTag { export class TelekinesisTag extends SerializableBattlerTag {
public override readonly tagType = BattlerTagType.TELEKINESIS; public override readonly tagType = BattlerTagType.TELEKINESIS;
constructor(sourceMove: MoveId) { constructor() {
super( super(BattlerTagType.TELEKINESIS, BattlerTagLapseType.TURN_END, 3, MoveId.TELEKINESIS, undefined, true);
BattlerTagType.TELEKINESIS,
[BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.AFTER_MOVE],
3,
sourceMove,
undefined,
true,
);
} }
override onAdd(pokemon: Pokemon) { override onAdd(pokemon: Pokemon) {
@ -3831,7 +3834,7 @@ export function getBattlerTag(
case BattlerTagType.SYRUP_BOMB: case BattlerTagType.SYRUP_BOMB:
return new SyrupBombTag(sourceId); return new SyrupBombTag(sourceId);
case BattlerTagType.TELEKINESIS: case BattlerTagType.TELEKINESIS:
return new TelekinesisTag(sourceMove); return new TelekinesisTag();
case BattlerTagType.POWER_TRICK: case BattlerTagType.POWER_TRICK:
return new PowerTrickTag(sourceMove, sourceId); return new PowerTrickTag(sourceMove, sourceId);
case BattlerTagType.GRUDGE: case BattlerTagType.GRUDGE:

View File

@ -283,9 +283,12 @@ export const invalidEncoreMoves: ReadonlySet<MoveId> = new Set([
]); ]);
/** /**
* Set of all {@linkcode SpeciesId}s that are barred from use by {@linkcode MoveId.TELEKINESIS}. * Set of all {@linkcode SpeciesId}s that {@linkcode MoveId.TELEKINESIS} cannot directly affect.
* They can still receive the effect from Baton Passing, however.
* *
* (Not included here is Gengar, which is only forbidden in its Mega form.) * @remarks
* Not included here is Gengar, which is only forbidden in its Mega form and which
* _cannot_ receive either of Telekinesis' effects via Baton Pass.
*/ */
export const invalidTelekinesisSpecies: ReadonlySet<SpeciesId> = new Set([ export const invalidTelekinesisSpecies: ReadonlySet<SpeciesId> = new Set([
SpeciesId.DIGLETT, SpeciesId.DIGLETT,
@ -296,4 +299,4 @@ export const invalidTelekinesisSpecies: ReadonlySet<SpeciesId> = new Set([
SpeciesId.PALOSSAND, SpeciesId.PALOSSAND,
SpeciesId.WIGLETT, SpeciesId.WIGLETT,
SpeciesId.WUGTRIO, SpeciesId.WUGTRIO,
] as const); ]);

View File

@ -77,7 +77,15 @@ import {
PreserveBerryModifier, PreserveBerryModifier,
} from "#modifiers/modifier"; } from "#modifiers/modifier";
import { applyMoveAttrs } from "#moves/apply-attrs"; import { applyMoveAttrs } from "#moves/apply-attrs";
import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSketchMoves, invalidSleepTalkMoves, invalidTelekinesisSpecies } from "#moves/invalid-moves"; import {
invalidAssistMoves,
invalidCopycatMoves,
invalidMetronomeMoves,
invalidMirrorMoveMoves,
invalidSketchMoves,
invalidSleepTalkMoves,
invalidTelekinesisSpecies,
} from "#moves/invalid-moves";
import { frenzyMissFunc, getMoveTargets } from "#moves/move-utils"; import { frenzyMissFunc, getMoveTargets } from "#moves/move-utils";
import { PokemonMove } from "#moves/pokemon-move"; import { PokemonMove } from "#moves/pokemon-move";
import { MoveEndPhase } from "#phases/move-end-phase"; import { MoveEndPhase } from "#phases/move-end-phase";
@ -10083,13 +10091,15 @@ export function initMoves() {
new StatusMove(MoveId.TELEKINESIS, PokemonType.PSYCHIC, -1, 15, -1, 0, 5) new StatusMove(MoveId.TELEKINESIS, PokemonType.PSYCHIC, -1, 15, -1, 0, 5)
.attr(AddBattlerTagAttr, BattlerTagType.TELEKINESIS, false, true, 3) .attr(AddBattlerTagAttr, BattlerTagType.TELEKINESIS, false, true, 3)
.attr(AddBattlerTagAttr, BattlerTagType.FLOATING, false, true, 3) .attr(AddBattlerTagAttr, BattlerTagType.FLOATING, false, true, 3)
.condition((_user, target, _move) => !( .condition((_user, target) => !(
invalidTelekinesisSpecies.has(target.species.speciesId) invalidTelekinesisSpecies.has(target.species.speciesId)
|| (target.species.speciesId === SpeciesId.GENGAR && target.getFormKey() === SpeciesFormKey.MEGA) || (target.species.speciesId === SpeciesId.GENGAR && target.getFormKey() === SpeciesFormKey.MEGA)
)) ))
.condition(failOnGravityCondition) .condition(failOnGravityCondition)
.condition(failOnGroundedCondition) .condition(failOnGroundedCondition)
.reflectable(), .reflectable()
// Still preserves FLOATING tag when Baton Passed onto a Mega Gengar
.edgeCase(),
new StatusMove(MoveId.MAGIC_ROOM, PokemonType.PSYCHIC, -1, 10, -1, 0, 5) new StatusMove(MoveId.MAGIC_ROOM, PokemonType.PSYCHIC, -1, 10, -1, 0, 5)
.ignoresProtect() .ignoresProtect()
.target(MoveTarget.BOTH_SIDES) .target(MoveTarget.BOTH_SIDES)

View File

@ -2284,7 +2284,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
/** /**
* Return whether this Pokemon is currently on the ground. * Return whether this Pokemon is currently on the ground.
* *
* To be considered grounded, a Pokemon must fulfill any of the following criteria: * To be considered grounded, a Pokemon must either:
* * Be {@linkcode BattlerTagType.IGNORE_FLYING | forcibly grounded} from an effect like Smack Down or Ingrain * * Be {@linkcode BattlerTagType.IGNORE_FLYING | forcibly grounded} from an effect like Smack Down or Ingrain
* * Be under the effects of {@linkcode ArenaTagType.GRAVITY | harsh gravity} * * Be under the effects of {@linkcode ArenaTagType.GRAVITY | harsh gravity}
* * **Not** be any of the following things: * * **Not** be any of the following things:
@ -4376,10 +4376,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
} }
/** /**
* Transferring stat changes and Tags * Transfer stat changes and volatile status effects upon a Pokemon switching in via Baton Pass.
* @param source {@linkcode Pokemon} the pokemon whose stats/Tags are to be passed on from, ie: the Pokemon using Baton Pass * Should be called prior to the recipient's summon data being reset.
* @param source - The {@linkcode Pokemon} using Baton Pass to source the effects from.
*/ */
transferSummon(source: Pokemon): void { public transferSummon(source: Pokemon): void {
// Copy all stat stages // Copy all stat stages
for (const s of BATTLE_STATS) { for (const s of BATTLE_STATS) {
const sourceStage = source.getStatStage(s); const sourceStage = source.getStatStage(s);
@ -4389,12 +4390,20 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
this.setStatStage(s, sourceStage); this.setStatStage(s, sourceStage);
} }
// Edge case: avoid transferring either of Telekinesis' effects when passing to Mega Gengar
// TODO: Rather than handling this logic in the core baton pass logic, move it to an
// overriddable helper function on the BattlerTag instance
const isMegaGengarReceivingTelekinesis =
this.species.speciesId === SpeciesId.GENGAR &&
this.getFormKey() === SpeciesFormKey.MEGA &&
!!source.getTag(BattlerTagType.TELEKINESIS);
// Copy all transferrable BattlerTags
for (const tag of source.summonData.tags) { for (const tag of source.summonData.tags) {
if ( if (
!tag.isBatonPassable || !tag.isBatonPassable ||
(tag.tagType === BattlerTagType.TELEKINESIS && (isMegaGengarReceivingTelekinesis &&
this.species.speciesId === SpeciesId.GENGAR && (tag.tagType === BattlerTagType.TELEKINESIS || tag.tagType === BattlerTagType.FLOATING))
this.getFormKey() === "mega")
) { ) {
continue; continue;
} }

View File

@ -27,7 +27,7 @@ describe("Arena - Gravity", () => {
.battleStyle("single") .battleStyle("single")
.ability(AbilityId.UNNERVE) .ability(AbilityId.UNNERVE)
.enemyAbility(AbilityId.BALL_FETCH) .enemyAbility(AbilityId.BALL_FETCH)
.enemySpecies(SpeciesId.MAGIKARP) .enemySpecies(SpeciesId.FLETCHLING)
.enemyLevel(5); .enemyLevel(5);
}); });
@ -41,7 +41,7 @@ describe("Arena - Gravity", () => {
await game.move.forceEnemyMove(MoveId.TACKLE); await game.move.forceEnemyMove(MoveId.TACKLE);
await game.toEndOfTurn(); await game.toEndOfTurn();
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined(); expect(game).toHaveArenaTag(ArenaTagType.GRAVITY);
expect(accSpy).toHaveLastReturnedWith(allMoves[MoveId.TACKLE].accuracy * 1.67); expect(accSpy).toHaveLastReturnedWith(allMoves[MoveId.TACKLE].accuracy * 1.67);
}); });
@ -62,11 +62,12 @@ describe("Arena - Gravity", () => {
const player = game.field.getPlayerPokemon(); const player = game.field.getPlayerPokemon();
const enemy = game.field.getEnemyPokemon(); const enemy = game.field.getEnemyPokemon();
expect(enemy.isGrounded()).toBe(false);
game.move.use(MoveId.GRAVITY); game.move.use(MoveId.GRAVITY);
await game.toNextTurn(); await game.toNextTurn();
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined(); expect(game).toHaveArenaTag(ArenaTagType.GRAVITY);
expect(player.isGrounded()).toBe(true); expect(player.isGrounded()).toBe(true);
expect(enemy.isGrounded()).toBe(true); expect(enemy.isGrounded()).toBe(true);
}); });

View File

@ -181,7 +181,7 @@ describe("Terrain -", () => {
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.toNextTurn(); await game.toNextTurn();
expect(enemy.getLastXMoves()[0].move).toBe(MoveId.FLY); expect(enemy).toHaveUsedMove(MoveId.FLY);
expect(powerSpy).toHaveLastReturnedWith(basePower); expect(powerSpy).toHaveLastReturnedWith(basePower);
}, },
); );
@ -201,11 +201,11 @@ describe("Terrain -", () => {
const pidgeot = game.field.getPlayerPokemon(); const pidgeot = game.field.getPlayerPokemon();
const shuckle = game.field.getEnemyPokemon(); const shuckle = game.field.getEnemyPokemon();
expect(pidgeot.status?.effect).toBe(StatusEffect.SLEEP); expect(pidgeot).toHaveStatusEffect(StatusEffect.SLEEP);
expect(shuckle.status?.effect).toBeUndefined(); expect(shuckle).toHaveStatusEffect(StatusEffect.NONE);
// TODO: These don't work due to how move failures are propagated // TODO: These don't work due to how move failures are propagated
// expect(pidgeot.getLastXMoves()[0].result).toBe(MoveResult.FAIL); // expect(pidgeot).toHaveUsedMove({ move: MoveId.SPORE, result: MoveResult.FAIL });
// expect(shuckle.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); // expect(shuckle).toHaveUsedMove({ move: MoveId.SPORE, result: MoveResult.SUCCESS });
expect(game.textInterceptor.logs).toContain( expect(game.textInterceptor.logs).toContain(
i18next.t("terrain:defaultBlockMessage", { i18next.t("terrain:defaultBlockMessage", {
@ -227,9 +227,9 @@ describe("Terrain -", () => {
await game.toEndOfTurn(); await game.toEndOfTurn();
const blissey = game.field.getPlayerPokemon(); const blissey = game.field.getPlayerPokemon();
expect(shuckle.status?.effect).toBeUndefined(); expect(shuckle).toHaveStatusEffect(StatusEffect.NONE);
expect(statusSpy).toHaveLastReturnedWith(false); expect(statusSpy).toHaveLastReturnedWith(false);
expect(blissey.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); expect(blissey).toHaveUsedMove({ move: MoveId.RELIC_SONG, result: MoveResult.SUCCESS });
expect(game.textInterceptor.logs).not.toContain( expect(game.textInterceptor.logs).not.toContain(
i18next.t("terrain:defaultBlockMessage", { i18next.t("terrain:defaultBlockMessage", {
@ -256,8 +256,8 @@ describe("Terrain -", () => {
const shuckle = game.field.getEnemyPokemon(); const shuckle = game.field.getEnemyPokemon();
// blissey is grounded & protected, shuckle isn't // blissey is grounded & protected, shuckle isn't
expect(blissey.status?.effect).toBeUndefined(); expect(blissey).toHaveStatusEffect(StatusEffect.NONE);
expect(shuckle.status?.effect).toBe(StatusEffect.TOXIC); expect(shuckle).toHaveStatusEffect(StatusEffect.TOXIC);
// TODO: These don't work due to how move failures are propagated // TODO: These don't work due to how move failures are propagated
// expect(blissey.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); // expect(blissey.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
// expect(shuckle.getLastXMoves()[0].result).toBe(MoveResult.FAIL); // expect(shuckle.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
@ -313,8 +313,8 @@ describe("Terrain -", () => {
// Blissey was grounded and protected from effect, but still took damage // Blissey was grounded and protected from effect, but still took damage
expect(blissey).not.toHaveFullHp(); expect(blissey).not.toHaveFullHp();
expect(blissey).not.toHaveBattlerTag(BattlerTagType.CONFUSED); expect(blissey).not.toHaveBattlerTag(BattlerTagType.CONFUSED);
expect(blissey.status?.effect).toBe(StatusEffect.NONE); expect(blissey).toHaveStatusEffect(StatusEffect.NONE);
expect(shuckle).toHaveUsedMove({ result: MoveResult.SUCCESS }); expect(shuckle).toHaveUsedMove({ move, result: MoveResult.SUCCESS });
expect(game.textInterceptor.logs).not.toContain( expect(game.textInterceptor.logs).not.toContain(
i18next.t("terrain:mistyBlockMessage", { i18next.t("terrain:mistyBlockMessage", {
@ -381,7 +381,7 @@ describe("Terrain -", () => {
category: "Enemy-targeting spread", category: "Enemy-targeting spread",
move: MoveId.DARK_VOID, move: MoveId.DARK_VOID,
effect: () => { effect: () => {
expect(game.field.getEnemyPokemon().status?.effect).toBe(StatusEffect.SLEEP); expect(game.field.getEnemyPokemon()).toHaveStatusEffect(StatusEffect.SLEEP);
}, },
}, },
])("should not block $category moves that become priority", async ({ move, effect }) => { ])("should not block $category moves that become priority", async ({ move, effect }) => {

View File

@ -67,17 +67,57 @@ describe("Move - Telekinesis", () => {
expect(enemy.isGrounded()).toBe(false); expect(enemy.isGrounded()).toBe(false);
}); });
// TODO: Verify whether the 3 turn duration includes the turn the move is used
it.todo("should last 3 turns", async () => {
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
game.move.use(MoveId.TELEKINESIS);
await game.phaseInterceptor.to("MoveEndPhase");
const enemy = game.field.getEnemyPokemon();
expect(enemy).toHaveBattlerTag({ tagType: BattlerTagType.TELEKINESIS, turnCount: 2 });
await game.toNextTurn();
game.move.use(MoveId.SPLASH);
await game.toNextTurn();
expect(enemy).toHaveBattlerTag({ tagType: BattlerTagType.TELEKINESIS, turnCount: 1 });
game.move.use(MoveId.SPLASH);
await game.toNextTurn();
expect(enemy).not.toHaveBattlerTag(BattlerTagType.TELEKINESIS);
});
const cases = ([BattlerTagType.TELEKINESIS, BattlerTagType.FLOATING, BattlerTagType.IGNORE_FLYING] as const).map(
t => ({
tagType: t,
name: getEnumStr(BattlerTagType, t),
}),
);
it.each(cases)("should fail if the target already has BattlerTagType.$name", async ({ tagType }) => {
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const enemy = game.field.getEnemyPokemon();
enemy.addTag(tagType);
game.move.use(MoveId.TELEKINESIS);
await game.toEndOfTurn();
const karp = game.field.getPlayerPokemon();
expect(karp).toHaveUsedMove({ move: MoveId.TELEKINESIS, result: MoveResult.FAIL });
});
const invalidSpecies = [...invalidTelekinesisSpecies].map(s => ({ const invalidSpecies = [...invalidTelekinesisSpecies].map(s => ({
species: s, species: s,
name: getEnumStr(SpeciesId, s, { casing: "Title" }), name: getEnumStr(SpeciesId, s),
})); }));
it.each(invalidSpecies)( it.each(invalidSpecies)(
"should fail if used on a $name, but can still be baton passed onto one", "should fail if used on $name, but can still be Baton Passed onto one",
async ({ species }) => { async ({ species }) => {
await game.classicMode.startBattle([species, SpeciesId.FEEBAS]); await game.classicMode.startBattle([species, SpeciesId.FEEBAS]);
const [invalidMon, feebas] = game.scene.getPlayerParty(); const [invalidMon, feebas] = game.scene.getPlayerParty();
expect(invalidMon.species.speciesId).toBe(species); expect(invalidTelekinesisSpecies).toContain(invalidMon.species.speciesId);
game.move.use(MoveId.TELEPORT); game.move.use(MoveId.TELEPORT);
game.doSelectPartyPokemon(1); game.doSelectPartyPokemon(1);
@ -90,6 +130,7 @@ describe("Move - Telekinesis", () => {
expect(invalidMon.isOnField()).toBe(false); expect(invalidMon.isOnField()).toBe(false);
// Turn 2: Transfer Telekinesis from Feebas to the invalid pokemon
game.move.use(MoveId.BATON_PASS); game.move.use(MoveId.BATON_PASS);
game.doSelectPartyPokemon(1); game.doSelectPartyPokemon(1);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
@ -115,13 +156,14 @@ describe("Move - Telekinesis", () => {
expect(enemy.summonData.speciesForm?.speciesId).toBe(SpeciesId.DIGLETT); expect(enemy.summonData.speciesForm?.speciesId).toBe(SpeciesId.DIGLETT);
game.move.use(MoveId.TELEKINESIS); game.move.use(MoveId.TELEKINESIS);
await game.move.forceEnemyMove(MoveId.SPLASH);
await game.toEndOfTurn(); await game.toEndOfTurn();
expect(enemy).toHaveBattlerTag(BattlerTagType.TELEKINESIS); expect(enemy).toHaveBattlerTag(BattlerTagType.TELEKINESIS);
expect(enemy).toHaveBattlerTag(BattlerTagType.FLOATING); expect(enemy).toHaveBattlerTag(BattlerTagType.FLOATING);
const feebas = game.field.getPlayerPokemon(); const feebas = game.field.getPlayerPokemon();
expect(feebas).toHaveUsedMove({ move: MoveId.TELEKINESIS, result: MoveResult.FAIL }); expect(feebas).toHaveUsedMove({ move: MoveId.TELEKINESIS, result: MoveResult.SUCCESS });
}); });
// TODO: Move to ingrain's test file // TODO: Move to ingrain's test file
@ -176,7 +218,7 @@ describe("Move - Telekinesis", () => {
await game.toEndOfTurn(); await game.toEndOfTurn();
// Should have not received tags // Should have not received either tag from baton passing
expect(gengar.isOnField()).toBe(true); expect(gengar.isOnField()).toBe(true);
expect(gengar).not.toHaveBattlerTag(BattlerTagType.TELEKINESIS); expect(gengar).not.toHaveBattlerTag(BattlerTagType.TELEKINESIS);
expect(gengar).not.toHaveBattlerTag(BattlerTagType.FLOATING); expect(gengar).not.toHaveBattlerTag(BattlerTagType.FLOATING);

View File

@ -33,16 +33,16 @@ export class MoveHelper extends GameManagerHelper {
} }
/** /**
* Intercepts {@linkcode MoveEffectPhase} and mocks the phase's move's accuracy * Intercepts the next upcoming {@linkcode MoveEffectPhase} and mocks the invoked move's accuracy
* to `0`, guaranteeing a miss. * to `0`, guaranteeing a miss.
* @param firstTargetOnly - Whether to only force a miss on the first target hit; default `false`. * @param firstTargetOnly - Whether to only force a miss on the first target hit; default `false`.
* @returns A promise that resolves once the next MoveEffectPhase has been reached (not run). * @returns A Promise that resolves once the next `MoveEffectPhase` has been reached (not run).
* @remarks * @remarks
* This is notably useful for testing guaranteed-hit granting effects, as it will ensure * This is notably useful for testing guaranteed-hit granting effects, as it will ensure
* a miss if the effect does not apply. * the attacking move misses if the effect does not apply.
*/ */
public async forceMiss(firstTargetOnly = false): Promise<void> { public async forceMiss(firstTargetOnly = false): Promise<void> {
await this.game.phaseInterceptor.to(MoveEffectPhase, false); await this.game.phaseInterceptor.to("MoveEffectPhase", false);
const moveEffectPhase = this.game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase; const moveEffectPhase = this.game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase;
const accuracy = vi.spyOn(moveEffectPhase.move, "calculateBattleAccuracy"); const accuracy = vi.spyOn(moveEffectPhase.move, "calculateBattleAccuracy");