mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-19 22:09:27 +02:00
Bug fixes + test fixes
This commit is contained in:
parent
2f26afd6fd
commit
58b83b1a55
@ -3447,7 +3447,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
if (effect === StatusEffect.SLEEP || effect === StatusEffect.FREEZE) {
|
||||
const currentPhase = this.scene.getCurrentPhase();
|
||||
if (currentPhase instanceof MoveEffectPhase && currentPhase.getUserPokemon() === this) {
|
||||
this.turnData.hitCount -= this.turnData.hitsLeft - 1;
|
||||
this.turnData.hitCount = 1;
|
||||
this.turnData.hitsLeft = 1;
|
||||
}
|
||||
}
|
||||
|
@ -81,8 +81,6 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
private firstHit: boolean;
|
||||
/** Is this the last strike of a move? */
|
||||
private lastHit: boolean;
|
||||
/** Is this the first target to be hit by this strike? */
|
||||
private firstTarget: boolean = true;
|
||||
|
||||
constructor(scene: BattleScene, battlerIndex: BattlerIndex, targets: BattlerIndex[], move: PokemonMove) {
|
||||
super(scene, battlerIndex);
|
||||
@ -136,90 +134,6 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
|
||||
user.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
|
||||
|
||||
this.moveHistoryEntry = { move: move.id, targets: this.targets, result: MoveResult.PENDING, virtual: this.move.virtual };
|
||||
|
||||
targets.forEach((t, i) => this.hitChecks[i] = this.hitCheck(t));
|
||||
|
||||
if (!targets.some(t => t.isActive(true))) {
|
||||
this.scene.queueMessage(i18next.t("battle:attackFailed"));
|
||||
this.moveHistoryEntry.result = MoveResult.FAIL;
|
||||
}
|
||||
|
||||
if (this.hitChecks.some(hc => hc[0] === HitCheckResult.HIT)) {
|
||||
this.moveHistoryEntry.result = MoveResult.SUCCESS;
|
||||
} else if (this.hitChecks.every(hc => hc[0] === HitCheckResult.MISS)) {
|
||||
this.moveHistoryEntry.result = MoveResult.MISS;
|
||||
} else {
|
||||
this.moveHistoryEntry.result = MoveResult.FAIL;
|
||||
}
|
||||
|
||||
// If the move has a post-target effect (e.g. Explosion), but doesn't
|
||||
// successfully hit a target, play the move's animation and return
|
||||
if (move.getAttrs(MoveEffectAttr).some(attr => attr.trigger === MoveEffectTrigger.POST_TARGET)
|
||||
&& this.hitChecks.every(hc => hc[1] === 0, this)) {
|
||||
const playOnEmptyField = this.scene.currentBattle?.mysteryEncounter?.hasBattleAnimationsWithoutTargets ?? false;
|
||||
return new MoveAnim(move.id, user, this.getFirstTarget()!.getBattlerIndex(), playOnEmptyField).play(this.scene, false, () =>
|
||||
this.triggerMoveEffects(MoveEffectTrigger.POST_TARGET, user, null).then(() => this.end())
|
||||
);
|
||||
}
|
||||
|
||||
// If this phase represents the first strike of the given move,
|
||||
// log the move in the user's move history.
|
||||
if (user.turnData.hitsLeft === -1) {
|
||||
user.pushMoveHistory(this.moveHistoryEntry);
|
||||
}
|
||||
|
||||
console.log(this.hitChecks);
|
||||
|
||||
targets.forEach((target, i) => {
|
||||
const [ hitCheckResult, effectiveness ] = this.hitChecks[i];
|
||||
|
||||
switch (hitCheckResult) {
|
||||
case HitCheckResult.HIT:
|
||||
this.applyMoveEffects(target, effectiveness);
|
||||
this.firstTarget = false;
|
||||
break;
|
||||
case HitCheckResult.NO_EFFECT:
|
||||
if (move.id === Moves.SHEER_COLD) {
|
||||
this.scene.queueMessage(i18next.t("battle:hitResultImmune", { pokemonName: getPokemonNameWithAffix(target) }));
|
||||
} else {
|
||||
this.scene.queueMessage(i18next.t("battle:hitResultNoEffect", { pokemonName: getPokemonNameWithAffix(target) }));
|
||||
}
|
||||
case HitCheckResult.PROTECTED:
|
||||
case HitCheckResult.NO_EFFECT_NO_MESSAGE:
|
||||
applyMoveAttrs(NoEffectAttr, user, target, move);
|
||||
break;
|
||||
case HitCheckResult.MISS:
|
||||
this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getFirstTarget() ? getPokemonNameWithAffix(this.getFirstTarget()!) : "" }));
|
||||
applyMoveAttrs(MissEffectAttr, user, target, move);
|
||||
break;
|
||||
case HitCheckResult.PENDING:
|
||||
case HitCheckResult.ERROR:
|
||||
console.log(`Unexpected hit check result ${HitCheckResult[hitCheckResult]}. Aborting phase.`);
|
||||
return this.end();
|
||||
}
|
||||
});
|
||||
|
||||
const doPostTarget = this.lastHit ? this.triggerMoveEffects(MoveEffectTrigger.POST_TARGET, user, null) : Promise.resolve();
|
||||
doPostTarget.then(() => {
|
||||
this.updateSubstitutes();
|
||||
this.end();
|
||||
});
|
||||
}
|
||||
|
||||
protected applyMoveEffects(target: Pokemon, effectiveness: TypeDamageMultiplier): void {
|
||||
const user = this.getUserPokemon();
|
||||
const move = this.move.getMove();
|
||||
|
||||
if (isNullOrUndefined(user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// prevent field-targeted moves from activating multiple times
|
||||
if (move.isFieldTarget() && target !== this.getTargets()[this.targets.length - 1]) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* If this phase is for the first hit of the invoked move,
|
||||
* resolve the move's total hit count. This block combines the
|
||||
@ -238,13 +152,109 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
user.turnData.hitsLeft = hitCount.value;
|
||||
}
|
||||
|
||||
this.firstHit = user.turnData.hitsLeft === user.turnData.hitCount;
|
||||
this.lastHit = user.turnData.hitsLeft === 1 || !this.getTargets().some(t => t.isActive(true));
|
||||
this.moveHistoryEntry = { move: move.id, targets: this.targets, result: MoveResult.PENDING, virtual: this.move.virtual };
|
||||
|
||||
const playOnEmptyField = this.scene.currentBattle?.mysteryEncounter?.hasBattleAnimationsWithoutTargets ?? false;
|
||||
return new MoveAnim(move.id as Moves, user, this.getFirstTarget()!.getBattlerIndex()!, playOnEmptyField).play(this.scene, move.hitsSubstitute(user, this.getFirstTarget()!), async () => {
|
||||
await this.triggerMoveEffects(MoveEffectTrigger.PRE_APPLY, user, target);
|
||||
targets.forEach((t, i) => this.hitChecks[i] = this.hitCheck(t));
|
||||
|
||||
if (!targets.some(t => t.isActive(true))) {
|
||||
this.scene.queueMessage(i18next.t("battle:attackFailed"));
|
||||
this.moveHistoryEntry.result = MoveResult.FAIL;
|
||||
}
|
||||
|
||||
if (this.hitChecks.some(hc => hc[0] === HitCheckResult.HIT)) {
|
||||
this.moveHistoryEntry.result = MoveResult.SUCCESS;
|
||||
} else {
|
||||
user.turnData.hitCount = 1;
|
||||
user.turnData.hitsLeft = 1;
|
||||
|
||||
if (this.hitChecks.every(hc => hc[0] === HitCheckResult.MISS)) {
|
||||
this.moveHistoryEntry.result = MoveResult.MISS;
|
||||
} else {
|
||||
this.moveHistoryEntry.result = MoveResult.FAIL;
|
||||
}
|
||||
}
|
||||
|
||||
this.firstHit = user.turnData.hitCount === user.turnData.hitsLeft;
|
||||
this.lastHit = user.turnData.hitsLeft === 1 || !targets.some(t => t.isActive(true));
|
||||
|
||||
// If the move successfully hit at least 1 target, or the move has a
|
||||
// post-target effect, play the move's animation
|
||||
const tryPlayAnim = (this.moveHistoryEntry.result === MoveResult.SUCCESS || move.getAttrs(MoveEffectAttr).some(attr => attr.trigger === MoveEffectTrigger.POST_TARGET))
|
||||
? this.playMoveAnim(user)
|
||||
: Promise.resolve();
|
||||
|
||||
tryPlayAnim.then(() => {
|
||||
// If this phase represents the first strike of the given move,
|
||||
// log the move in the user's move history.
|
||||
if (this.firstHit) {
|
||||
user.pushMoveHistory(this.moveHistoryEntry);
|
||||
}
|
||||
|
||||
const applyPromises: Promise<void>[] = [];
|
||||
|
||||
for (const target of targets) {
|
||||
const [ hitCheckResult, effectiveness ] = this.hitChecks[targets.indexOf(target)];
|
||||
|
||||
switch (hitCheckResult) {
|
||||
case HitCheckResult.HIT:
|
||||
applyPromises.push(this.applyMoveEffects(target, effectiveness));
|
||||
break;
|
||||
case HitCheckResult.NO_EFFECT:
|
||||
if (move.id === Moves.SHEER_COLD) {
|
||||
this.scene.queueMessage(i18next.t("battle:hitResultImmune", { pokemonName: getPokemonNameWithAffix(target) }));
|
||||
} else {
|
||||
this.scene.queueMessage(i18next.t("battle:hitResultNoEffect", { pokemonName: getPokemonNameWithAffix(target) }));
|
||||
}
|
||||
case HitCheckResult.PROTECTED:
|
||||
case HitCheckResult.NO_EFFECT_NO_MESSAGE:
|
||||
applyMoveAttrs(NoEffectAttr, user, target, move);
|
||||
break;
|
||||
case HitCheckResult.MISS:
|
||||
this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getFirstTarget() ? getPokemonNameWithAffix(this.getFirstTarget()!) : "" }));
|
||||
applyMoveAttrs(MissEffectAttr, user, target, move);
|
||||
break;
|
||||
case HitCheckResult.PENDING:
|
||||
case HitCheckResult.ERROR:
|
||||
console.log(`Unexpected hit check result ${HitCheckResult[hitCheckResult]}. Aborting phase.`);
|
||||
return this.end();
|
||||
}
|
||||
}
|
||||
|
||||
Promise.allSettled(applyPromises)
|
||||
.then(() => executeIf(this.lastHit, () => this.triggerMoveEffects(MoveEffectTrigger.POST_TARGET, user, null)))
|
||||
.then(() => {
|
||||
this.updateSubstitutes();
|
||||
this.end();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected playMoveAnim(user: Pokemon): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const move = this.move.getMove();
|
||||
const firstTargetPokemon = this.getFirstTarget() ?? null;
|
||||
const playOnEmptyField = this.scene.currentBattle?.mysteryEncounter?.hasBattleAnimationsWithoutTargets ?? false;
|
||||
new MoveAnim(move.id, user, firstTargetPokemon!.getBattlerIndex(), playOnEmptyField)
|
||||
.play(this.scene, move.hitsSubstitute(user, firstTargetPokemon), () => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
protected applyMoveEffects(target: Pokemon, effectiveness: TypeDamageMultiplier): Promise<void> {
|
||||
const user = this.getUserPokemon();
|
||||
const move = this.move.getMove();
|
||||
|
||||
const firstTarget = target === this.getTargets().find((_, i) => this.hitChecks[i][1] > 0);
|
||||
|
||||
if (isNullOrUndefined(user)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// prevent field-targeted moves from activating multiple times
|
||||
if (move.isFieldTarget() && target !== this.getTargets()[this.targets.length - 1]) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return this.triggerMoveEffects(MoveEffectTrigger.PRE_APPLY, user, target).then(() => {
|
||||
const hitResult = this.applyMove(target, effectiveness);
|
||||
|
||||
/** Does {@linkcode hitResult} indicate that damage was dealt to the target? */
|
||||
@ -255,19 +265,20 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
HitResult.ONE_HIT_KO
|
||||
].includes(hitResult);
|
||||
|
||||
await this.triggerMoveEffects(MoveEffectTrigger.POST_APPLY, user, target);
|
||||
this.applyHeldItemFlinchCheck(user, target, dealsDamage);
|
||||
await this.triggerMoveEffects(MoveEffectTrigger.HIT, user, target);
|
||||
await this.applyOnGetHitAbEffects(user, target, hitResult);
|
||||
await applyPostAttackAbAttrs(PostAttackAbAttr, user, target, move, hitResult);
|
||||
return this.triggerMoveEffects(MoveEffectTrigger.POST_APPLY, user, target, firstTarget)
|
||||
.then(() => this.applyHeldItemFlinchCheck(user, target, dealsDamage))
|
||||
.then(() => this.triggerMoveEffects(MoveEffectTrigger.HIT, user, target, firstTarget))
|
||||
.then(() => this.applyOnGetHitAbEffects(user, target, hitResult))
|
||||
.then(() => applyPostAttackAbAttrs(PostAttackAbAttr, user, target, move, hitResult))
|
||||
.then(() => {
|
||||
if (move instanceof AttackMove) {
|
||||
this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target);
|
||||
}
|
||||
|
||||
if (move instanceof AttackMove) {
|
||||
this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target);
|
||||
}
|
||||
|
||||
if (this.lastHit) {
|
||||
this.scene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger);
|
||||
}
|
||||
if (this.lastHit) {
|
||||
this.scene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -307,18 +318,19 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
* @param triggerType The {@linkcode MoveEffectTrigger} being applied
|
||||
* @param user The {@linkcode Pokemon} using the move
|
||||
* @param target The {@linkcode Pokemon} targeted by the move
|
||||
* @param firstTarget Whether the target is the first to be hit by the current strike
|
||||
* @param selfTarget If defined, limits the effects triggered to either self-targeted
|
||||
* effects (if set to `true`) or targeted effects (if set to `false`).
|
||||
* @returns a `Promise` applying the relevant move effects.
|
||||
*/
|
||||
protected triggerMoveEffects(triggerType: MoveEffectTrigger, user: Pokemon, target: Pokemon | null, selfTarget?: boolean): Promise<void> {
|
||||
protected triggerMoveEffects(triggerType: MoveEffectTrigger, user: Pokemon, target: Pokemon | null, firstTarget?: boolean | null, selfTarget?: boolean): Promise<void> {
|
||||
return applyFilteredMoveAttrs((attr: MoveAttr) =>
|
||||
attr instanceof MoveEffectAttr
|
||||
&& attr.trigger === triggerType
|
||||
&& (isNullOrUndefined(selfTarget) || (attr.selfTarget === selfTarget))
|
||||
&& (!attr.firstHitOnly || this.firstHit)
|
||||
&& (!attr.lastHitOnly || this.lastHit)
|
||||
&& (!attr.firstTargetOnly || this.firstTarget),
|
||||
&& (!attr.firstTargetOnly || (firstTarget ?? true)),
|
||||
user, target, this.move.getMove());
|
||||
}
|
||||
|
||||
@ -565,7 +577,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
}
|
||||
}
|
||||
|
||||
if (alwaysHit || target.getTag(BattlerTagType.TELEKINESIS) && !move.hasAttr(OneHitKOAttr)) {
|
||||
if (alwaysHit || (target.getTag(BattlerTagType.TELEKINESIS) && !move.hasAttr(OneHitKOAttr))) {
|
||||
return [ HitCheckResult.HIT, effectiveness ];
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,6 @@ import { Type } from "#enums/type";
|
||||
import { Abilities } from "#app/enums/abilities";
|
||||
import { Moves } from "#app/enums/moves";
|
||||
import { Species } from "#app/enums/species";
|
||||
import { HitResult } from "#app/field/pokemon";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
@ -39,13 +38,13 @@ describe("Abilities - Galvanize", () => {
|
||||
});
|
||||
|
||||
it("should change Normal-type attacks to Electric type and boost their power", async () => {
|
||||
await game.startBattle();
|
||||
await game.classicMode.startBattle([ Species.MAGIKARP ]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
vi.spyOn(playerPokemon, "getMoveType");
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
vi.spyOn(enemyPokemon, "apply");
|
||||
vi.spyOn(enemyPokemon, "getMoveEffectiveness");
|
||||
|
||||
const move = allMoves[Moves.TACKLE];
|
||||
vi.spyOn(move, "calculateBattlePower");
|
||||
@ -55,7 +54,7 @@ describe("Abilities - Galvanize", () => {
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(playerPokemon.getMoveType).toHaveLastReturnedWith(Type.ELECTRIC);
|
||||
expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.EFFECTIVE);
|
||||
expect(enemyPokemon.getMoveEffectiveness).toHaveReturnedWith(1);
|
||||
expect(move.calculateBattlePower).toHaveReturnedWith(48);
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
||||
});
|
||||
@ -63,13 +62,13 @@ describe("Abilities - Galvanize", () => {
|
||||
it("should cause Normal-type attacks to activate Volt Absorb", async () => {
|
||||
game.override.enemyAbility(Abilities.VOLT_ABSORB);
|
||||
|
||||
await game.startBattle();
|
||||
await game.classicMode.startBattle([ Species.MAGIKARP ]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
vi.spyOn(playerPokemon, "getMoveType");
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
vi.spyOn(enemyPokemon, "apply");
|
||||
vi.spyOn(enemyPokemon, "getMoveEffectiveness");
|
||||
|
||||
enemyPokemon.hp = Math.floor(enemyPokemon.getMaxHp() * 0.8);
|
||||
|
||||
@ -78,37 +77,37 @@ describe("Abilities - Galvanize", () => {
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(playerPokemon.getMoveType).toHaveLastReturnedWith(Type.ELECTRIC);
|
||||
expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.NO_EFFECT);
|
||||
expect(enemyPokemon.getMoveEffectiveness).toHaveReturnedWith(0);
|
||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
||||
});
|
||||
|
||||
it("should not change the type of variable-type moves", async () => {
|
||||
game.override.enemySpecies(Species.MIGHTYENA);
|
||||
|
||||
await game.startBattle([ Species.ESPEON ]);
|
||||
await game.classicMode.startBattle([ Species.ESPEON ]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
vi.spyOn(playerPokemon, "getMoveType");
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
vi.spyOn(enemyPokemon, "apply");
|
||||
vi.spyOn(enemyPokemon, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(Moves.REVELATION_DANCE);
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(playerPokemon.getMoveType).not.toHaveLastReturnedWith(Type.ELECTRIC);
|
||||
expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.NO_EFFECT);
|
||||
expect(enemyPokemon.getMoveEffectiveness).toHaveReturnedWith(0);
|
||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
||||
});
|
||||
|
||||
it("should affect all hits of a Normal-type multi-hit move", async () => {
|
||||
await game.startBattle();
|
||||
await game.classicMode.startBattle([ Species.MAGIKARP ]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
vi.spyOn(playerPokemon, "getMoveType");
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
vi.spyOn(enemyPokemon, "apply");
|
||||
vi.spyOn(enemyPokemon, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(Moves.FURY_SWIPES);
|
||||
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
|
||||
@ -126,6 +125,6 @@ describe("Abilities - Galvanize", () => {
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp);
|
||||
}
|
||||
|
||||
expect(enemyPokemon.apply).not.toHaveReturnedWith(HitResult.NO_EFFECT);
|
||||
expect(enemyPokemon.getMoveEffectiveness).not.toHaveReturnedWith(0);
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
|
||||
import { HitCheckResult, MoveEffectPhase } from "#app/phases/move-effect-phase";
|
||||
import { MoveEndPhase } from "#app/phases/move-end-phase";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
@ -28,6 +28,7 @@ describe("Abilities - No Guard", () => {
|
||||
.moveset(Moves.ZAP_CANNON)
|
||||
.ability(Abilities.NO_GUARD)
|
||||
.enemyLevel(200)
|
||||
.enemySpecies(Species.SNORLAX)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(Moves.SPLASH);
|
||||
});
|
||||
@ -50,7 +51,7 @@ describe("Abilities - No Guard", () => {
|
||||
|
||||
await game.phaseInterceptor.to(MoveEndPhase);
|
||||
|
||||
expect(moveEffectPhase.hitCheck).toHaveReturnedWith(true);
|
||||
expect(moveEffectPhase.hitCheck).toHaveReturnedWith([ HitCheckResult.HIT, 1 ]);
|
||||
});
|
||||
|
||||
it("should guarantee double battle with any one LURE", async () => {
|
||||
|
@ -1,16 +1,17 @@
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { applyAbAttrs, applyPostDefendAbAttrs, applyPreAttackAbAttrs, MoveEffectChanceMultiplierAbAttr, MovePowerBoostAbAttr, PostDefendTypeChangeAbAttr } from "#app/data/ability";
|
||||
import { applyAbAttrs, applyPreAttackAbAttrs, MoveEffectChanceMultiplierAbAttr, MovePowerBoostAbAttr } from "#app/data/ability";
|
||||
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
|
||||
import { NumberHolder } from "#app/utils";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { Type } from "#enums/type";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { allMoves } from "#app/data/move";
|
||||
import { HitResult } from "#app/field/pokemon";
|
||||
|
||||
|
||||
describe("Abilities - Sheer Force", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -130,40 +131,25 @@ describe("Abilities - Sheer Force", () => {
|
||||
|
||||
it("Sheer Force Disabling Specific Abilities", async () => {
|
||||
const moveToUse = Moves.CRUSH_CLAW;
|
||||
game.override.enemyAbility(Abilities.COLOR_CHANGE);
|
||||
game.override.startingHeldItems([{ name: "KINGS_ROCK", count: 1 }]);
|
||||
game.override.ability(Abilities.SHEER_FORCE);
|
||||
await game.startBattle([ Species.PIDGEOT ]);
|
||||
game.override
|
||||
.startingLevel(100)
|
||||
.enemyLevel(100)
|
||||
.enemySpecies(Species.SHUCKLE)
|
||||
.enemyAbility(Abilities.COLOR_CHANGE)
|
||||
.ability(Abilities.SHEER_FORCE);
|
||||
|
||||
await game.classicMode.startBattle([ Species.PIDGEOT ]);
|
||||
|
||||
game.scene.getEnemyPokemon()!.stats[Stat.DEF] = 10000;
|
||||
expect(game.scene.getPlayerPokemon()!.formIndex).toBe(0);
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(moveToUse);
|
||||
|
||||
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
|
||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
|
||||
const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
|
||||
const move = phase.move.getMove();
|
||||
expect(move.id).toBe(Moves.CRUSH_CLAW);
|
||||
|
||||
//Disable color change due to being hit by Sheer Force
|
||||
const power = new NumberHolder(move.power);
|
||||
const chance = new NumberHolder(move.chance);
|
||||
const user = phase.getUserPokemon()!;
|
||||
const target = phase.getFirstTarget()!;
|
||||
const opponentType = target.getTypes()[0];
|
||||
|
||||
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, false, chance, move, target, false);
|
||||
applyPreAttackAbAttrs(MovePowerBoostAbAttr, user, target, move, false, power);
|
||||
applyPostDefendAbAttrs(PostDefendTypeChangeAbAttr, target, user, move, HitResult.EFFECTIVE);
|
||||
|
||||
expect(chance.value).toBe(0);
|
||||
expect(power.value).toBe(move.power * 5461 / 4096);
|
||||
expect(target.getTypes().length).toBe(2);
|
||||
expect(target.getTypes()[0]).toBe(opponentType);
|
||||
await game.move.forceHit();
|
||||
await game.phaseInterceptor.to("MoveEndPhase", false);
|
||||
|
||||
expect(player.battleData.abilitiesApplied.includes(Abilities.SHEER_FORCE)).toBeTruthy();
|
||||
expect(enemy.getTypes()).toEqual([ Type.BUG, Type.ROCK ]);
|
||||
}, 20000);
|
||||
|
||||
it("Two Pokemon with abilities disabled by Sheer Force hitting each other should not cause a crash", async () => {
|
||||
|
@ -2,7 +2,6 @@ import { BattlerIndex } from "#app/battle";
|
||||
import { Abilities } from "#app/enums/abilities";
|
||||
import { Moves } from "#app/enums/moves";
|
||||
import { Species } from "#app/enums/species";
|
||||
import { HitResult } from "#app/field/pokemon";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
@ -98,12 +97,12 @@ describe("Abilities - Tera Shell", () => {
|
||||
await game.classicMode.startBattle([ Species.CHARIZARD ]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
vi.spyOn(playerPokemon, "apply");
|
||||
vi.spyOn(playerPokemon, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
expect(playerPokemon.apply).toHaveLastReturnedWith(HitResult.EFFECTIVE);
|
||||
expect(playerPokemon.getMoveEffectiveness).toHaveLastReturnedWith(1);
|
||||
expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp() - 40);
|
||||
}
|
||||
);
|
||||
@ -116,7 +115,9 @@ describe("Abilities - Tera Shell", () => {
|
||||
await game.classicMode.startBattle([ Species.SNORLAX ]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
vi.spyOn(playerPokemon, "apply");
|
||||
vi.spyOn(playerPokemon, "getMoveEffectiveness");
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
|
||||
@ -124,9 +125,9 @@ describe("Abilities - Tera Shell", () => {
|
||||
await game.move.forceHit();
|
||||
for (let i = 0; i < 2; i++) {
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
expect(playerPokemon.apply).toHaveLastReturnedWith(HitResult.NOT_VERY_EFFECTIVE);
|
||||
expect(playerPokemon.getMoveEffectiveness).toHaveLastReturnedWith(0.5);
|
||||
}
|
||||
expect(playerPokemon.apply).toHaveReturnedTimes(2);
|
||||
expect(enemyPokemon.turnData.hitCount).toBe(2);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -36,8 +36,7 @@ describe("Items - Dire Hit", () => {
|
||||
.enemyMoveset(Moves.SPLASH)
|
||||
.moveset([ Moves.POUND ])
|
||||
.startingHeldItems([{ name: "DIRE_HIT" }])
|
||||
.battleType("single")
|
||||
.disableCrits();
|
||||
.battleType("single");
|
||||
|
||||
}, 20000);
|
||||
|
||||
|
@ -28,7 +28,6 @@ describe("Items - Leek", () => {
|
||||
.enemyMoveset([ Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH ])
|
||||
.startingHeldItems([{ name: "LEEK" }])
|
||||
.moveset([ Moves.TACKLE ])
|
||||
.disableCrits()
|
||||
.battleType("single");
|
||||
});
|
||||
|
||||
|
@ -27,8 +27,7 @@ describe("Items - Scope Lens", () => {
|
||||
.enemyMoveset(Moves.SPLASH)
|
||||
.moveset([ Moves.POUND ])
|
||||
.startingHeldItems([{ name: "SCOPE_LENS" }])
|
||||
.battleType("single")
|
||||
.disableCrits();
|
||||
.battleType("single");
|
||||
|
||||
}, 20000);
|
||||
|
||||
|
@ -3,7 +3,6 @@ import { Stat } from "#enums/stat";
|
||||
import { allMoves } from "#app/data/move";
|
||||
import { Type } from "#enums/type";
|
||||
import { Abilities } from "#app/enums/abilities";
|
||||
import { HitResult } from "#app/field/pokemon";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
@ -47,21 +46,21 @@ describe("Moves - Tera Blast", () => {
|
||||
game.override
|
||||
.enemySpecies(Species.FURRET)
|
||||
.startingHeldItems([{ name: "TERA_SHARD", type: Type.FIGHTING }]);
|
||||
await game.startBattle();
|
||||
await game.classicMode.startBattle();
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
vi.spyOn(enemyPokemon, "apply");
|
||||
vi.spyOn(enemyPokemon, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(Moves.TERA_BLAST);
|
||||
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.SUPER_EFFECTIVE);
|
||||
expect(enemyPokemon.getMoveEffectiveness).toHaveReturnedWith(2);
|
||||
}, 20000);
|
||||
|
||||
it("increases power if user is Stellar tera type", async () => {
|
||||
game.override.startingHeldItems([{ name: "TERA_SHARD", type: Type.STELLAR }]);
|
||||
|
||||
await game.startBattle();
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
game.move.select(Moves.TERA_BLAST);
|
||||
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
|
||||
@ -73,23 +72,23 @@ describe("Moves - Tera Blast", () => {
|
||||
it("is super effective against terastallized targets if user is Stellar tera type", async () => {
|
||||
game.override.startingHeldItems([{ name: "TERA_SHARD", type: Type.STELLAR }]);
|
||||
|
||||
await game.startBattle();
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
vi.spyOn(enemyPokemon, "apply");
|
||||
vi.spyOn(enemyPokemon, "getMoveEffectiveness");
|
||||
vi.spyOn(enemyPokemon, "isTerastallized").mockReturnValue(true);
|
||||
|
||||
game.move.select(Moves.TERA_BLAST);
|
||||
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.SUPER_EFFECTIVE);
|
||||
expect(enemyPokemon.getMoveEffectiveness).toHaveReturnedWith(2);
|
||||
});
|
||||
|
||||
// Currently abilities are bugged and can't see when a move's category is changed
|
||||
it.skip("uses the higher stat of the user's Atk and SpAtk for damage calculation", async () => {
|
||||
game.override.enemyAbility(Abilities.TOXIC_DEBRIS);
|
||||
await game.startBattle();
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
playerPokemon.stats[Stat.ATK] = 100;
|
||||
@ -102,7 +101,7 @@ describe("Moves - Tera Blast", () => {
|
||||
|
||||
it("causes stat drops if user is Stellar tera type", async () => {
|
||||
game.override.startingHeldItems([{ name: "TERA_SHARD", type: Type.STELLAR }]);
|
||||
await game.startBattle();
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
|
@ -14,26 +14,24 @@ import { vi } from "vitest";
|
||||
*/
|
||||
export class MoveHelper extends GameManagerHelper {
|
||||
/**
|
||||
* Intercepts {@linkcode MoveEffectPhase} and mocks the
|
||||
* {@linkcode MoveEffectPhase.hitCheck | hitCheck}'s return value to `true`.
|
||||
* Used to force a move to hit.
|
||||
* Intercepts {@linkcode MoveEffectPhase} and mocks the phase's move's
|
||||
* accuracy to -1, guaranteeing a hit.
|
||||
*/
|
||||
public async forceHit(): Promise<void> {
|
||||
await this.game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
const moveEffectPhase = this.game.scene.getCurrentPhase() as MoveEffectPhase;
|
||||
vi.spyOn(moveEffectPhase.move.getMove(), "accuracy", "get").mockReturnValue(-1);
|
||||
vi.spyOn(moveEffectPhase.move.getMove(), "calculateBattleAccuracy").mockReturnValue(-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercepts {@linkcode MoveEffectPhase} and mocks the
|
||||
* {@linkcode MoveEffectPhase.hitCheck | hitCheck}'s return value to `false`.
|
||||
* Used to force a move to miss.
|
||||
* Intercepts {@linkcode MoveEffectPhase} and mocks the phase's move's accuracy
|
||||
* to 0, guaranteeing a miss.
|
||||
* @param firstTargetOnly - Whether the move should force miss on the first target only, in the case of multi-target moves.
|
||||
*/
|
||||
public async forceMiss(firstTargetOnly: boolean = false): Promise<void> {
|
||||
await this.game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
const moveEffectPhase = this.game.scene.getCurrentPhase() as MoveEffectPhase;
|
||||
const accuracy = vi.spyOn(moveEffectPhase.move.getMove(), "accuracy", "get");
|
||||
const accuracy = vi.spyOn(moveEffectPhase.move.getMove(), "calculateBattleAccuracy");
|
||||
|
||||
if (firstTargetOnly) {
|
||||
accuracy.mockReturnValueOnce(0);
|
||||
|
Loading…
Reference in New Issue
Block a user