Bug fixes + test fixes

This commit is contained in:
innerthunder 2024-11-16 15:30:27 -08:00
parent 2f26afd6fd
commit 58b83b1a55
11 changed files with 174 additions and 181 deletions

View File

@ -3447,7 +3447,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (effect === StatusEffect.SLEEP || effect === StatusEffect.FREEZE) { if (effect === StatusEffect.SLEEP || effect === StatusEffect.FREEZE) {
const currentPhase = this.scene.getCurrentPhase(); const currentPhase = this.scene.getCurrentPhase();
if (currentPhase instanceof MoveEffectPhase && currentPhase.getUserPokemon() === this) { if (currentPhase instanceof MoveEffectPhase && currentPhase.getUserPokemon() === this) {
this.turnData.hitCount -= this.turnData.hitsLeft - 1; this.turnData.hitCount = 1;
this.turnData.hitsLeft = 1; this.turnData.hitsLeft = 1;
} }
} }

View File

@ -81,8 +81,6 @@ export class MoveEffectPhase extends PokemonPhase {
private firstHit: boolean; private firstHit: boolean;
/** Is this the last strike of a move? */ /** Is this the last strike of a move? */
private lastHit: boolean; 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) { constructor(scene: BattleScene, battlerIndex: BattlerIndex, targets: BattlerIndex[], move: PokemonMove) {
super(scene, battlerIndex); super(scene, battlerIndex);
@ -136,90 +134,6 @@ export class MoveEffectPhase extends PokemonPhase {
user.lapseTags(BattlerTagLapseType.MOVE_EFFECT); 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, * If this phase is for the first hit of the invoked move,
* resolve the move's total hit count. This block combines the * 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; user.turnData.hitsLeft = hitCount.value;
} }
this.firstHit = user.turnData.hitsLeft === user.turnData.hitCount; this.moveHistoryEntry = { move: move.id, targets: this.targets, result: MoveResult.PENDING, virtual: this.move.virtual };
this.lastHit = user.turnData.hitsLeft === 1 || !this.getTargets().some(t => t.isActive(true));
const playOnEmptyField = this.scene.currentBattle?.mysteryEncounter?.hasBattleAnimationsWithoutTargets ?? false; targets.forEach((t, i) => this.hitChecks[i] = this.hitCheck(t));
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);
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); const hitResult = this.applyMove(target, effectiveness);
/** Does {@linkcode hitResult} indicate that damage was dealt to the target? */ /** Does {@linkcode hitResult} indicate that damage was dealt to the target? */
@ -255,19 +265,20 @@ export class MoveEffectPhase extends PokemonPhase {
HitResult.ONE_HIT_KO HitResult.ONE_HIT_KO
].includes(hitResult); ].includes(hitResult);
await this.triggerMoveEffects(MoveEffectTrigger.POST_APPLY, user, target); return this.triggerMoveEffects(MoveEffectTrigger.POST_APPLY, user, target, firstTarget)
this.applyHeldItemFlinchCheck(user, target, dealsDamage); .then(() => this.applyHeldItemFlinchCheck(user, target, dealsDamage))
await this.triggerMoveEffects(MoveEffectTrigger.HIT, user, target); .then(() => this.triggerMoveEffects(MoveEffectTrigger.HIT, user, target, firstTarget))
await this.applyOnGetHitAbEffects(user, target, hitResult); .then(() => this.applyOnGetHitAbEffects(user, target, hitResult))
await applyPostAttackAbAttrs(PostAttackAbAttr, user, target, move, 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) { if (this.lastHit) {
this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target); 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 triggerType The {@linkcode MoveEffectTrigger} being applied
* @param user The {@linkcode Pokemon} using the move * @param user The {@linkcode Pokemon} using the move
* @param target The {@linkcode Pokemon} targeted by 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 * @param selfTarget If defined, limits the effects triggered to either self-targeted
* effects (if set to `true`) or targeted effects (if set to `false`). * effects (if set to `true`) or targeted effects (if set to `false`).
* @returns a `Promise` applying the relevant move effects. * @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) => return applyFilteredMoveAttrs((attr: MoveAttr) =>
attr instanceof MoveEffectAttr attr instanceof MoveEffectAttr
&& attr.trigger === triggerType && attr.trigger === triggerType
&& (isNullOrUndefined(selfTarget) || (attr.selfTarget === selfTarget)) && (isNullOrUndefined(selfTarget) || (attr.selfTarget === selfTarget))
&& (!attr.firstHitOnly || this.firstHit) && (!attr.firstHitOnly || this.firstHit)
&& (!attr.lastHitOnly || this.lastHit) && (!attr.lastHitOnly || this.lastHit)
&& (!attr.firstTargetOnly || this.firstTarget), && (!attr.firstTargetOnly || (firstTarget ?? true)),
user, target, this.move.getMove()); 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 ]; return [ HitCheckResult.HIT, effectiveness ];
} }

View File

@ -4,7 +4,6 @@ import { Type } from "#enums/type";
import { Abilities } from "#app/enums/abilities"; import { Abilities } from "#app/enums/abilities";
import { Moves } from "#app/enums/moves"; import { Moves } from "#app/enums/moves";
import { Species } from "#app/enums/species"; import { Species } from "#app/enums/species";
import { HitResult } from "#app/field/pokemon";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; 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 () => { 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()!; const playerPokemon = game.scene.getPlayerPokemon()!;
vi.spyOn(playerPokemon, "getMoveType"); vi.spyOn(playerPokemon, "getMoveType");
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
vi.spyOn(enemyPokemon, "apply"); vi.spyOn(enemyPokemon, "getMoveEffectiveness");
const move = allMoves[Moves.TACKLE]; const move = allMoves[Moves.TACKLE];
vi.spyOn(move, "calculateBattlePower"); vi.spyOn(move, "calculateBattlePower");
@ -55,7 +54,7 @@ describe("Abilities - Galvanize", () => {
await game.phaseInterceptor.to("BerryPhase", false); await game.phaseInterceptor.to("BerryPhase", false);
expect(playerPokemon.getMoveType).toHaveLastReturnedWith(Type.ELECTRIC); expect(playerPokemon.getMoveType).toHaveLastReturnedWith(Type.ELECTRIC);
expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.EFFECTIVE); expect(enemyPokemon.getMoveEffectiveness).toHaveReturnedWith(1);
expect(move.calculateBattlePower).toHaveReturnedWith(48); expect(move.calculateBattlePower).toHaveReturnedWith(48);
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
}); });
@ -63,13 +62,13 @@ describe("Abilities - Galvanize", () => {
it("should cause Normal-type attacks to activate Volt Absorb", async () => { it("should cause Normal-type attacks to activate Volt Absorb", async () => {
game.override.enemyAbility(Abilities.VOLT_ABSORB); game.override.enemyAbility(Abilities.VOLT_ABSORB);
await game.startBattle(); await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!; const playerPokemon = game.scene.getPlayerPokemon()!;
vi.spyOn(playerPokemon, "getMoveType"); vi.spyOn(playerPokemon, "getMoveType");
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
vi.spyOn(enemyPokemon, "apply"); vi.spyOn(enemyPokemon, "getMoveEffectiveness");
enemyPokemon.hp = Math.floor(enemyPokemon.getMaxHp() * 0.8); enemyPokemon.hp = Math.floor(enemyPokemon.getMaxHp() * 0.8);
@ -78,37 +77,37 @@ describe("Abilities - Galvanize", () => {
await game.phaseInterceptor.to("BerryPhase", false); await game.phaseInterceptor.to("BerryPhase", false);
expect(playerPokemon.getMoveType).toHaveLastReturnedWith(Type.ELECTRIC); expect(playerPokemon.getMoveType).toHaveLastReturnedWith(Type.ELECTRIC);
expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.NO_EFFECT); expect(enemyPokemon.getMoveEffectiveness).toHaveReturnedWith(0);
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
}); });
it("should not change the type of variable-type moves", async () => { it("should not change the type of variable-type moves", async () => {
game.override.enemySpecies(Species.MIGHTYENA); game.override.enemySpecies(Species.MIGHTYENA);
await game.startBattle([ Species.ESPEON ]); await game.classicMode.startBattle([ Species.ESPEON ]);
const playerPokemon = game.scene.getPlayerPokemon()!; const playerPokemon = game.scene.getPlayerPokemon()!;
vi.spyOn(playerPokemon, "getMoveType"); vi.spyOn(playerPokemon, "getMoveType");
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
vi.spyOn(enemyPokemon, "apply"); vi.spyOn(enemyPokemon, "getMoveEffectiveness");
game.move.select(Moves.REVELATION_DANCE); game.move.select(Moves.REVELATION_DANCE);
await game.phaseInterceptor.to("BerryPhase", false); await game.phaseInterceptor.to("BerryPhase", false);
expect(playerPokemon.getMoveType).not.toHaveLastReturnedWith(Type.ELECTRIC); 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()); expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
}); });
it("should affect all hits of a Normal-type multi-hit move", async () => { 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()!; const playerPokemon = game.scene.getPlayerPokemon()!;
vi.spyOn(playerPokemon, "getMoveType"); vi.spyOn(playerPokemon, "getMoveType");
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
vi.spyOn(enemyPokemon, "apply"); vi.spyOn(enemyPokemon, "getMoveEffectiveness");
game.move.select(Moves.FURY_SWIPES); game.move.select(Moves.FURY_SWIPES);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
@ -126,6 +125,6 @@ describe("Abilities - Galvanize", () => {
expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp); expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp);
} }
expect(enemyPokemon.apply).not.toHaveReturnedWith(HitResult.NO_EFFECT); expect(enemyPokemon.getMoveEffectiveness).not.toHaveReturnedWith(0);
}); });
}); });

View File

@ -1,5 +1,5 @@
import { BattlerIndex } from "#app/battle"; 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 { MoveEndPhase } from "#app/phases/move-end-phase";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
@ -28,6 +28,7 @@ describe("Abilities - No Guard", () => {
.moveset(Moves.ZAP_CANNON) .moveset(Moves.ZAP_CANNON)
.ability(Abilities.NO_GUARD) .ability(Abilities.NO_GUARD)
.enemyLevel(200) .enemyLevel(200)
.enemySpecies(Species.SNORLAX)
.enemyAbility(Abilities.BALL_FETCH) .enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH); .enemyMoveset(Moves.SPLASH);
}); });
@ -50,7 +51,7 @@ describe("Abilities - No Guard", () => {
await game.phaseInterceptor.to(MoveEndPhase); 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 () => { it("should guarantee double battle with any one LURE", async () => {

View File

@ -1,16 +1,17 @@
import { BattlerIndex } from "#app/battle"; 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 { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { NumberHolder } from "#app/utils"; import { NumberHolder } from "#app/utils";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { Type } from "#enums/type";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { allMoves } from "#app/data/move"; import { allMoves } from "#app/data/move";
import { HitResult } from "#app/field/pokemon";
describe("Abilities - Sheer Force", () => { describe("Abilities - Sheer Force", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
@ -130,40 +131,25 @@ describe("Abilities - Sheer Force", () => {
it("Sheer Force Disabling Specific Abilities", async () => { it("Sheer Force Disabling Specific Abilities", async () => {
const moveToUse = Moves.CRUSH_CLAW; const moveToUse = Moves.CRUSH_CLAW;
game.override.enemyAbility(Abilities.COLOR_CHANGE); game.override
game.override.startingHeldItems([{ name: "KINGS_ROCK", count: 1 }]); .startingLevel(100)
game.override.ability(Abilities.SHEER_FORCE); .enemyLevel(100)
await game.startBattle([ Species.PIDGEOT ]); .enemySpecies(Species.SHUCKLE)
.enemyAbility(Abilities.COLOR_CHANGE)
.ability(Abilities.SHEER_FORCE);
await game.classicMode.startBattle([ Species.PIDGEOT ]);
game.scene.getEnemyPokemon()!.stats[Stat.DEF] = 10000; const player = game.scene.getPlayerPokemon()!;
expect(game.scene.getPlayerPokemon()!.formIndex).toBe(0); const enemy = game.scene.getEnemyPokemon()!;
game.move.select(moveToUse); game.move.select(moveToUse);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.move.forceHit();
await game.phaseInterceptor.to("MoveEndPhase", 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);
expect(player.battleData.abilitiesApplied.includes(Abilities.SHEER_FORCE)).toBeTruthy();
expect(enemy.getTypes()).toEqual([ Type.BUG, Type.ROCK ]);
}, 20000); }, 20000);
it("Two Pokemon with abilities disabled by Sheer Force hitting each other should not cause a crash", async () => { it("Two Pokemon with abilities disabled by Sheer Force hitting each other should not cause a crash", async () => {

View File

@ -2,7 +2,6 @@ import { BattlerIndex } from "#app/battle";
import { Abilities } from "#app/enums/abilities"; import { Abilities } from "#app/enums/abilities";
import { Moves } from "#app/enums/moves"; import { Moves } from "#app/enums/moves";
import { Species } from "#app/enums/species"; import { Species } from "#app/enums/species";
import { HitResult } from "#app/field/pokemon";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
@ -98,12 +97,12 @@ describe("Abilities - Tera Shell", () => {
await game.classicMode.startBattle([ Species.CHARIZARD ]); await game.classicMode.startBattle([ Species.CHARIZARD ]);
const playerPokemon = game.scene.getPlayerPokemon()!; const playerPokemon = game.scene.getPlayerPokemon()!;
vi.spyOn(playerPokemon, "apply"); vi.spyOn(playerPokemon, "getMoveEffectiveness");
game.move.select(Moves.SPLASH); game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("BerryPhase", false); await game.phaseInterceptor.to("BerryPhase", false);
expect(playerPokemon.apply).toHaveLastReturnedWith(HitResult.EFFECTIVE); expect(playerPokemon.getMoveEffectiveness).toHaveLastReturnedWith(1);
expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp() - 40); expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp() - 40);
} }
); );
@ -116,7 +115,9 @@ describe("Abilities - Tera Shell", () => {
await game.classicMode.startBattle([ Species.SNORLAX ]); await game.classicMode.startBattle([ Species.SNORLAX ]);
const playerPokemon = game.scene.getPlayerPokemon()!; const playerPokemon = game.scene.getPlayerPokemon()!;
vi.spyOn(playerPokemon, "apply"); vi.spyOn(playerPokemon, "getMoveEffectiveness");
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.SPLASH); game.move.select(Moves.SPLASH);
@ -124,9 +125,9 @@ describe("Abilities - Tera Shell", () => {
await game.move.forceHit(); await game.move.forceHit();
for (let i = 0; i < 2; i++) { for (let i = 0; i < 2; i++) {
await game.phaseInterceptor.to("MoveEffectPhase"); 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);
} }
); );
}); });

View File

@ -36,8 +36,7 @@ describe("Items - Dire Hit", () => {
.enemyMoveset(Moves.SPLASH) .enemyMoveset(Moves.SPLASH)
.moveset([ Moves.POUND ]) .moveset([ Moves.POUND ])
.startingHeldItems([{ name: "DIRE_HIT" }]) .startingHeldItems([{ name: "DIRE_HIT" }])
.battleType("single") .battleType("single");
.disableCrits();
}, 20000); }, 20000);

View File

@ -28,7 +28,6 @@ describe("Items - Leek", () => {
.enemyMoveset([ Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH ]) .enemyMoveset([ Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH ])
.startingHeldItems([{ name: "LEEK" }]) .startingHeldItems([{ name: "LEEK" }])
.moveset([ Moves.TACKLE ]) .moveset([ Moves.TACKLE ])
.disableCrits()
.battleType("single"); .battleType("single");
}); });

View File

@ -27,8 +27,7 @@ describe("Items - Scope Lens", () => {
.enemyMoveset(Moves.SPLASH) .enemyMoveset(Moves.SPLASH)
.moveset([ Moves.POUND ]) .moveset([ Moves.POUND ])
.startingHeldItems([{ name: "SCOPE_LENS" }]) .startingHeldItems([{ name: "SCOPE_LENS" }])
.battleType("single") .battleType("single");
.disableCrits();
}, 20000); }, 20000);

View File

@ -3,7 +3,6 @@ import { Stat } from "#enums/stat";
import { allMoves } from "#app/data/move"; import { allMoves } from "#app/data/move";
import { Type } from "#enums/type"; import { Type } from "#enums/type";
import { Abilities } from "#app/enums/abilities"; import { Abilities } from "#app/enums/abilities";
import { HitResult } from "#app/field/pokemon";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
@ -47,21 +46,21 @@ describe("Moves - Tera Blast", () => {
game.override game.override
.enemySpecies(Species.FURRET) .enemySpecies(Species.FURRET)
.startingHeldItems([{ name: "TERA_SHARD", type: Type.FIGHTING }]); .startingHeldItems([{ name: "TERA_SHARD", type: Type.FIGHTING }]);
await game.startBattle(); await game.classicMode.startBattle();
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
vi.spyOn(enemyPokemon, "apply"); vi.spyOn(enemyPokemon, "getMoveEffectiveness");
game.move.select(Moves.TERA_BLAST); game.move.select(Moves.TERA_BLAST);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("MoveEffectPhase"); await game.phaseInterceptor.to("MoveEffectPhase");
expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.SUPER_EFFECTIVE); expect(enemyPokemon.getMoveEffectiveness).toHaveReturnedWith(2);
}, 20000); }, 20000);
it("increases power if user is Stellar tera type", async () => { it("increases power if user is Stellar tera type", async () => {
game.override.startingHeldItems([{ name: "TERA_SHARD", type: Type.STELLAR }]); game.override.startingHeldItems([{ name: "TERA_SHARD", type: Type.STELLAR }]);
await game.startBattle(); await game.classicMode.startBattle();
game.move.select(Moves.TERA_BLAST); game.move.select(Moves.TERA_BLAST);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); 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 () => { it("is super effective against terastallized targets if user is Stellar tera type", async () => {
game.override.startingHeldItems([{ name: "TERA_SHARD", type: Type.STELLAR }]); game.override.startingHeldItems([{ name: "TERA_SHARD", type: Type.STELLAR }]);
await game.startBattle(); await game.classicMode.startBattle();
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
vi.spyOn(enemyPokemon, "apply"); vi.spyOn(enemyPokemon, "getMoveEffectiveness");
vi.spyOn(enemyPokemon, "isTerastallized").mockReturnValue(true); vi.spyOn(enemyPokemon, "isTerastallized").mockReturnValue(true);
game.move.select(Moves.TERA_BLAST); game.move.select(Moves.TERA_BLAST);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("MoveEffectPhase"); 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 // 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 () => { it.skip("uses the higher stat of the user's Atk and SpAtk for damage calculation", async () => {
game.override.enemyAbility(Abilities.TOXIC_DEBRIS); game.override.enemyAbility(Abilities.TOXIC_DEBRIS);
await game.startBattle(); await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!; const playerPokemon = game.scene.getPlayerPokemon()!;
playerPokemon.stats[Stat.ATK] = 100; playerPokemon.stats[Stat.ATK] = 100;
@ -102,7 +101,7 @@ describe("Moves - Tera Blast", () => {
it("causes stat drops if user is Stellar tera type", async () => { it("causes stat drops if user is Stellar tera type", async () => {
game.override.startingHeldItems([{ name: "TERA_SHARD", type: Type.STELLAR }]); game.override.startingHeldItems([{ name: "TERA_SHARD", type: Type.STELLAR }]);
await game.startBattle(); await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!; const playerPokemon = game.scene.getPlayerPokemon()!;

View File

@ -14,26 +14,24 @@ import { vi } from "vitest";
*/ */
export class MoveHelper extends GameManagerHelper { export class MoveHelper extends GameManagerHelper {
/** /**
* Intercepts {@linkcode MoveEffectPhase} and mocks the * Intercepts {@linkcode MoveEffectPhase} and mocks the phase's move's
* {@linkcode MoveEffectPhase.hitCheck | hitCheck}'s return value to `true`. * accuracy to -1, guaranteeing a hit.
* Used to force a move to hit.
*/ */
public async forceHit(): Promise<void> { public async forceHit(): Promise<void> {
await this.game.phaseInterceptor.to(MoveEffectPhase, false); await this.game.phaseInterceptor.to(MoveEffectPhase, false);
const moveEffectPhase = this.game.scene.getCurrentPhase() as MoveEffectPhase; 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 * Intercepts {@linkcode MoveEffectPhase} and mocks the phase's move's accuracy
* {@linkcode MoveEffectPhase.hitCheck | hitCheck}'s return value to `false`. * to 0, guaranteeing a miss.
* Used to force a move to miss.
* @param firstTargetOnly - Whether the move should force miss on the first target only, in the case of multi-target moves. * @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> { public async forceMiss(firstTargetOnly: boolean = false): Promise<void> {
await this.game.phaseInterceptor.to(MoveEffectPhase, false); await this.game.phaseInterceptor.to(MoveEffectPhase, false);
const moveEffectPhase = this.game.scene.getCurrentPhase() as MoveEffectPhase; 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) { if (firstTargetOnly) {
accuracy.mockReturnValueOnce(0); accuracy.mockReturnValueOnce(0);