Compare commits

...

9 Commits

Author SHA1 Message Date
Bertie690
65b036fccd
Merge b393cf8914 into 4b70fab608 2025-06-20 10:03:53 -04:00
NightKev
4b70fab608
[Bug] Remove message for Rock Head activation (#6014) 2025-06-19 20:59:55 -07:00
Bertie690
b393cf8914
Merge branch 'beta' into test-cleanup 2025-06-19 20:05:44 -04:00
Bertie690
f231755db2 fixed using sizzly slide instead of will o wisp to inflict burn 2025-06-17 18:27:27 -04:00
Bertie690
6d777bb7c8 Fixed innards out 2025-06-17 17:14:44 -04:00
Bertie690
fcd270ed9b fixed tests 2025-06-17 17:08:43 -04:00
Bertie690
86a3f6d711 Merge remote-tracking branch 'upstream/beta' into test-cleanup 2025-06-17 17:03:40 -04:00
Bertie690
12a626d2a5 Fixde magic guard aftermath 2025-06-15 10:25:49 -04:00
Bertie690
be3c42a531 Fixed tests for Magic Guard, Battle phase; fixed innards out stuff 2025-06-13 13:45:54 -04:00
5 changed files with 235 additions and 441 deletions

View File

@ -306,13 +306,6 @@ export class BlockRecoilDamageAttr extends AbAttr {
): void {
cancelled.value = true;
}
getTriggerMessage(pokemon: Pokemon, abilityName: string, ..._args: any[]) {
return i18next.t("abilityTriggers:blockRecoilDamage", {
pokemonName: getPokemonNameWithAffix(pokemon),
abilityName: abilityName,
});
}
}
/**
@ -6542,13 +6535,26 @@ export class PostFaintContactDamageAbAttr extends PostFaintAbAttr {
_hitResult?: HitResult,
..._args: any[]
): boolean {
const diedToDirectDamage =
move !== undefined &&
attacker !== undefined &&
move.doesFlagEffectApply({ flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon });
// Return early if ability user did not die to a direct-contact attack.
if (
move === undefined ||
attacker === undefined ||
!move.doesFlagEffectApply({ flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon })
) {
return false;
}
const cancelled = new BooleanHolder(false);
globalScene.getField(true).map(p => applyAbAttrs("FieldPreventExplosiveMovesAbAttr", p, cancelled, simulated));
return !(!diedToDirectDamage || cancelled.value || attacker!.hasAbilityWithAttr("BlockNonDirectDamageAbAttr"));
// TODO: This should be in speed order
globalScene.getField(true).forEach(p => applyAbAttrs("FieldPreventExplosiveMovesAbAttr", p, cancelled, simulated));
if (cancelled.value) {
return false;
}
// TODO: Does aftermath display text if the attacker has Magic Guard?
applyAbAttrs("BlockNonDirectDamageAbAttr", attacker, cancelled);
return !cancelled.value;
}
override applyPostFaint(
@ -6557,15 +6563,15 @@ export class PostFaintContactDamageAbAttr extends PostFaintAbAttr {
simulated: boolean,
attacker?: Pokemon,
_move?: Move,
_hitResult?: HitResult,
..._args: any[]
): void {
if (!simulated) {
attacker!.damageAndUpdate(toDmgValue(attacker!.getMaxHp() * (1 / this.damageRatio)), {
result: HitResult.INDIRECT,
});
attacker!.turnData.damageTaken += toDmgValue(attacker!.getMaxHp() * (1 / this.damageRatio));
if (simulated) {
return;
}
attacker!.damageAndUpdate(toDmgValue(attacker!.getMaxHp() * (1 / this.damageRatio)), {
result: HitResult.INDIRECT,
});
attacker!.turnData.damageTaken += toDmgValue(attacker!.getMaxHp() * (1 / this.damageRatio));
}
getTriggerMessage(pokemon: Pokemon, abilityName: string, ..._args: any[]): string {
@ -6577,27 +6583,38 @@ export class PostFaintContactDamageAbAttr extends PostFaintAbAttr {
}
/**
* Attribute used for abilities (Innards Out) that damage the opponent based on how much HP the last attack used to knock out the owner of the ability.
* Attribute used for abilities that damage an opponent who faints the ability holder
* equal to the amount of damage the last attack inflicted.
*
* Used for {@linkcode Abilities.INNARDS_OUT}.
*/
export class PostFaintHPDamageAbAttr extends PostFaintAbAttr {
override applyPostFaint(
pokemon: Pokemon,
_passive: boolean,
simulated: boolean,
_simulated: boolean,
attacker?: Pokemon,
move?: Move,
_hitResult?: HitResult,
..._args: any[]
): void {
//If the mon didn't die to indirect damage
if (move !== undefined && attacker !== undefined && !simulated) {
const damage = pokemon.turnData.attacksReceived[0].damage;
attacker.damageAndUpdate(damage, { result: HitResult.INDIRECT });
attacker.turnData.damageTaken += damage;
// return early if the user died to indirect damage, target has magic guard or was KO'd by an ally
if (move === undefined || attacker === undefined || attacker.getAlly() === pokemon) {
return;
}
// TODO: Confirm that magic guard's flyout shows here?
const cancelled = new BooleanHolder(false);
applyAbAttrs("BlockNonDirectDamageAbAttr", attacker, cancelled);
if (cancelled.value) {
return;
}
const damage = pokemon.turnData.attacksReceived[0].damage;
attacker.damageAndUpdate(damage, { result: HitResult.INDIRECT });
attacker.turnData.damageTaken += damage;
}
getTriggerMessage(pokemon: Pokemon, abilityName: string, ..._args: any[]): string {
// Oddly, Innards Out still shows a flyout if the effect was blocked due to Magic Guard...
override getTriggerMessage(pokemon: Pokemon, abilityName: string): string {
return i18next.t("abilityTriggers:postFaintHpDamage", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
abilityName,

View File

@ -0,0 +1,62 @@
import { AbilityId } from "#enums/ability-id";
import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id";
import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Abilities - Innards Out", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.ability(AbilityId.BALL_FETCH)
.battleStyle("single")
.criticalHits(false)
.enemySpecies(SpeciesId.MAGIKARP)
.enemyAbility(AbilityId.INNARDS_OUT)
.enemyMoveset(MoveId.SPLASH)
.startingLevel(100);
});
it("should damage opppnents that faint the ability holder for equal damage", async () => {
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
const magikarp = game.field.getEnemyPokemon();
magikarp.hp = 20;
game.move.use(MoveId.X_SCISSOR);
await game.toEndOfTurn();
expect(magikarp.isFainted()).toBe(true);
const feebas = game.field.getPlayerPokemon();
expect(feebas.getInverseHp()).toBe(20);
});
it("should not damage an ally in Double Battles", async () => {
game.override.battleStyle("double");
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
const [magikarp1, magikarp2] = game.scene.getEnemyField();
magikarp1.hp = 1;
game.move.use(MoveId.PROTECT);
await game.move.forceEnemyMove(MoveId.SPLASH);
await game.move.forceEnemyMove(MoveId.SURF);
await game.toEndOfTurn();
expect(magikarp1.isFainted()).toBe(true);
expect(magikarp2.getInverseHp()).toBe(0);
});
});

View File

@ -1,19 +1,18 @@
import { getArenaTag } from "#app/data/arena-tag";
import { BattlerIndex } from "#enums/battler-index";
import { ArenaTagSide } from "#enums/arena-tag-side";
import { allMoves } from "#app/data/data-lists";
import { getStatusEffectCatchRateMultiplier } from "#app/data/status-effect";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { toDmgValue } from "#app/utils/common";
import { AbilityId } from "#enums/ability-id";
import { ArenaTagType } from "#enums/arena-tag-type";
import { BattlerTagType } from "#enums/battler-tag-type";
import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id";
import { StatusEffect } from "#enums/status-effect";
import { WeatherType } from "#enums/weather-type";
import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Abilities - Magic Guard", () => {
describe("AbilityId - Magic Guard", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
@ -29,404 +28,142 @@ describe("Abilities - Magic Guard", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
/** Player Pokemon overrides */
.ability(AbilityId.MAGIC_GUARD)
.moveset([MoveId.SPLASH])
.enemySpecies(SpeciesId.BLISSEY)
.enemyAbility(AbilityId.NO_GUARD)
.startingLevel(100)
/** Enemy Pokemon overrides */
.enemySpecies(SpeciesId.SNORLAX)
.enemyAbility(AbilityId.INSOMNIA)
.enemyMoveset(MoveId.SPLASH)
.enemyLevel(100);
});
//Bulbapedia Reference: https://bulbapedia.bulbagarden.net/wiki/Magic_Guard_(Ability)
it("ability should prevent damage caused by weather", async () => {
game.override.weather(WeatherType.SANDSTORM);
it.each<{ name: string; move?: MoveId; enemyMove?: MoveId }>([
{ name: "Non-Volatile Status Conditions", enemyMove: MoveId.TOXIC },
{ name: "Volatile Status Conditions", enemyMove: MoveId.LEECH_SEED },
{ name: "Crash Damage", move: MoveId.HIGH_JUMP_KICK },
{ name: "Variable Recoil Moves", move: MoveId.DOUBLE_EDGE },
{ name: "HP% Recoil Moves", move: MoveId.CHLOROBLAST },
])("should prevent damage from $name", async ({ move = MoveId.SPLASH, enemyMove = MoveId.SPLASH }) => {
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
// force a miss on HJK
vi.spyOn(allMoves[MoveId.HIGH_JUMP_KICK], "accuracy", "get").mockReturnValue(0);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.use(move);
await game.move.forceEnemyMove(enemyMove);
await game.toEndOfTurn();
const enemyPokemon = game.scene.getEnemyPokemon()!;
expect(enemyPokemon).toBeDefined();
game.move.select(MoveId.SPLASH);
await game.phaseInterceptor.to(TurnEndPhase);
/**
* Expect:
* - The player Pokemon (with Magic Guard) has not taken damage from weather
* - The enemy Pokemon (without Magic Guard) has taken damage from weather
*/
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
const magikarp = game.field.getPlayerPokemon();
expect(magikarp.hp).toBe(magikarp.getMaxHp());
});
it("ability should prevent damage caused by status effects but other non-damage effects still apply", async () => {
//Toxic keeps track of the turn counters -> important that Magic Guard keeps track of post-Toxic turns
game.override.statusEffect(StatusEffect.POISON);
it.each<{ abName: string; move?: MoveId; enemyMove?: MoveId; passive?: AbilityId; enemyAbility?: AbilityId }>([
{ abName: "Bad Dreams", enemyMove: MoveId.SPORE, enemyAbility: AbilityId.BAD_DREAMS },
{ abName: "Aftermath", move: MoveId.PSYCHIC_FANGS, enemyAbility: AbilityId.AFTERMATH },
{ abName: "Innards Out", move: MoveId.PSYCHIC_FANGS, enemyAbility: AbilityId.INNARDS_OUT },
{ abName: "Rough Skin", move: MoveId.PSYCHIC_FANGS, enemyAbility: AbilityId.ROUGH_SKIN },
{ abName: "Dry Skin", move: MoveId.SUNNY_DAY, passive: AbilityId.DRY_SKIN },
{ abName: "Liquid Ooze", move: MoveId.DRAIN_PUNCH, enemyAbility: AbilityId.LIQUID_OOZE },
])(
"should prevent damage from $abName",
async ({
move = MoveId.SPLASH,
enemyMove = MoveId.SPLASH,
passive = AbilityId.BALL_FETCH,
enemyAbility = AbilityId.BALL_FETCH,
}) => {
game.override.enemyLevel(1).passiveAbility(passive).enemyAbility(enemyAbility);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
game.move.use(move);
await game.move.forceEnemyMove(enemyMove);
await game.toEndOfTurn();
const magikarp = game.field.getPlayerPokemon();
expect(magikarp.hp).toBe(magikarp.getMaxHp());
},
);
it.each<{ name: string; move?: MoveId; enemyMove?: MoveId }>([
{ name: "Struggle recoil", move: MoveId.STRUGGLE },
{ name: "Self-induced HP cutting", move: MoveId.BELLY_DRUM },
{ name: "Confusion self-damage", enemyMove: MoveId.CONFUSE_RAY },
])("should not prevent damage from $name", async ({ move = MoveId.SPLASH, enemyMove = MoveId.SPLASH }) => {
game.override.confusionActivation(true);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.use(move);
await game.move.forceEnemyMove(enemyMove);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); // For confuse ray
await game.toEndOfTurn();
game.move.select(MoveId.SPLASH);
await game.phaseInterceptor.to(TurnEndPhase);
/**
* Expect:
* - The player Pokemon (with Magic Guard) has not taken damage from poison
* - The Pokemon's CatchRateMultiplier should be 1.5
*/
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
expect(getStatusEffectCatchRateMultiplier(leadPokemon.status!.effect)).toBe(1.5);
});
it("ability effect should not persist when the ability is replaced", async () => {
game.override.enemyMoveset(MoveId.WORRY_SEED).statusEffect(StatusEffect.POISON);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.SPLASH);
await game.phaseInterceptor.to(TurnEndPhase);
/**
* Expect:
* - The player Pokemon (that just lost its Magic Guard ability) has taken damage from poison
*/
const leadPokemon = game.field.getPlayerPokemon();
expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp());
});
it("Magic Guard prevents damage caused by burn but other non-damaging effects are still applied", async () => {
game.override.enemyStatusEffect(StatusEffect.BURN).enemyAbility(AbilityId.MAGIC_GUARD);
it("should preserve toxic turn count and deal appropriate damage when disabled", async () => {
game.override.statusEffect(StatusEffect.TOXIC);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
game.move.select(MoveId.SPLASH);
game.move.use(MoveId.SPLASH);
await game.move.forceEnemyMove(MoveId.SPLASH);
await game.toNextTurn();
const enemyPokemon = game.scene.getEnemyPokemon()!;
const magikarp = game.field.getPlayerPokemon();
expect(magikarp.hp).toBe(magikarp.getMaxHp());
expect(magikarp.status?.toxicTurnCount).toBe(1);
await game.phaseInterceptor.to(TurnEndPhase);
// have a few turns pass
game.move.use(MoveId.SPLASH);
await game.toNextTurn();
game.move.use(MoveId.SPLASH);
await game.toNextTurn();
game.move.use(MoveId.SPLASH);
await game.toNextTurn();
expect(magikarp.status?.toxicTurnCount).toBe(4);
/**
* Expect:
* - The enemy Pokemon (with Magic Guard) has not taken damage from burn
* - The enemy Pokemon's physical attack damage is halved (TBD)
* - The enemy Pokemon's hypothetical CatchRateMultiplier should be 1.5
*/
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
expect(getStatusEffectCatchRateMultiplier(enemyPokemon.status!.effect)).toBe(1.5);
game.move.use(MoveId.SPLASH);
await game.move.forceEnemyMove(MoveId.GASTRO_ACID);
await game.toNextTurn();
expect(magikarp.status?.toxicTurnCount).toBe(5);
expect(magikarp.getHpRatio(true)).toBeCloseTo(11 / 16, 1);
});
it("Magic Guard prevents damage caused by toxic but other non-damaging effects are still applied", async () => {
game.override.enemyStatusEffect(StatusEffect.TOXIC).enemyAbility(AbilityId.MAGIC_GUARD);
it("should preserve burn physical damage halving & status catch boost", async () => {
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
game.move.select(MoveId.SPLASH);
// NB: Burn applies directly to the physical dmg formula, so we can't just check attack here
game.move.use(MoveId.TACKLE);
await game.move.forceEnemyMove(MoveId.WILL_O_WISP);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.toNextTurn();
const enemyPokemon = game.scene.getEnemyPokemon()!;
const magikarp = game.field.getPlayerPokemon();
expect(magikarp.hp).toBe(magikarp.getMaxHp());
expect(magikarp.status?.effect).toBe(StatusEffect.BURN);
const toxicStartCounter = enemyPokemon.status!.toxicTurnCount;
//should be 0
const blissey = game.field.getEnemyPokemon();
const prevDmg = blissey.getInverseHp();
blissey.hp = blissey.getMaxHp();
await game.phaseInterceptor.to(TurnEndPhase);
expect(getStatusEffectCatchRateMultiplier(magikarp.status!.effect)).toBe(1.5);
/**
* Expect:
* - The enemy Pokemon (with Magic Guard) has not taken damage from toxic
* - The enemy Pokemon's status effect duration should be incremented
* - The enemy Pokemon's hypothetical CatchRateMultiplier should be 1.5
*/
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
expect(enemyPokemon.status!.toxicTurnCount).toBeGreaterThan(toxicStartCounter);
expect(getStatusEffectCatchRateMultiplier(enemyPokemon.status!.effect)).toBe(1.5);
game.move.use(MoveId.TACKLE);
await game.toNextTurn();
const burntDmg = blissey.getInverseHp();
expect(burntDmg).toBeCloseTo(toDmgValue(prevDmg / 2), 0);
});
it("Magic Guard prevents damage caused by entry hazards", async () => {
//Adds and applies Spikes to both sides of the arena
const newTag = getArenaTag(ArenaTagType.SPIKES, 5, MoveId.SPIKES, 0, 0, ArenaTagSide.BOTH)!;
game.scene.arena.tags.push(newTag);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.SPLASH);
const enemyPokemon = game.scene.getEnemyPokemon()!;
await game.phaseInterceptor.to(TurnEndPhase);
/**
* Expect:
* - The player Pokemon (with Magic Guard) has not taken damage from spikes
* - The enemy Pokemon (without Magic Guard) has taken damage from spikes
*/
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
});
it("Magic Guard does not prevent poison from Toxic Spikes", async () => {
//Adds and applies Spikes to both sides of the arena
const playerTag = getArenaTag(ArenaTagType.TOXIC_SPIKES, 5, MoveId.TOXIC_SPIKES, 0, 0, ArenaTagSide.PLAYER)!;
const enemyTag = getArenaTag(ArenaTagType.TOXIC_SPIKES, 5, MoveId.TOXIC_SPIKES, 0, 0, ArenaTagSide.ENEMY)!;
game.scene.arena.tags.push(playerTag);
game.scene.arena.tags.push(enemyTag);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.SPLASH);
const enemyPokemon = game.scene.getEnemyPokemon()!;
await game.phaseInterceptor.to(TurnEndPhase);
/**
* Expect:
* - Both Pokemon gain the poison status effect
* - The player Pokemon (with Magic Guard) has not taken damage from poison
* - The enemy Pokemon (without Magic Guard) has taken damage from poison
*/
expect(leadPokemon.status!.effect).toBe(StatusEffect.POISON);
expect(enemyPokemon.status!.effect).toBe(StatusEffect.POISON);
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
});
it("Magic Guard prevents against damage from volatile status effects", async () => {
await game.classicMode.startBattle([SpeciesId.DUSKULL]);
game.override.moveset([MoveId.CURSE]).enemyAbility(AbilityId.MAGIC_GUARD);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.CURSE);
const enemyPokemon = game.scene.getEnemyPokemon()!;
await game.phaseInterceptor.to(TurnEndPhase);
/**
* Expect:
* - The player Pokemon (with Magic Guard) has cut its HP to inflict curse
* - The enemy Pokemon (with Magic Guard) is cursed
* - The enemy Pokemon (with Magic Guard) does not lose HP from being cursed
*/
expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp());
expect(enemyPokemon.getTag(BattlerTagType.CURSED)).not.toBe(undefined);
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
});
it("Magic Guard prevents crash damage", async () => {
game.override.moveset([MoveId.HIGH_JUMP_KICK]);
it("should prevent damage from entry hazards, but not Toxic Spikes poison", async () => {
game.scene.arena.addTag(ArenaTagType.SPIKES, -1, MoveId.SPIKES, 0, ArenaTagSide.PLAYER);
game.scene.arena.addTag(ArenaTagType.TOXIC_SPIKES, -1, MoveId.TOXIC_SPIKES, 0, ArenaTagSide.PLAYER);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.HIGH_JUMP_KICK);
await game.move.forceMiss();
await game.phaseInterceptor.to(TurnEndPhase);
/**
* Expect:
* - The player Pokemon (with Magic Guard) misses High Jump Kick but does not lose HP as a result
*/
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
});
it("Magic Guard prevents damage from recoil", async () => {
game.override.moveset([MoveId.TAKE_DOWN]);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.TAKE_DOWN);
await game.phaseInterceptor.to(TurnEndPhase);
/**
* Expect:
* - The player Pokemon (with Magic Guard) uses a recoil move but does not lose HP from recoil
*/
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
});
it("Magic Guard does not prevent damage from Struggle's recoil", async () => {
game.override.moveset([MoveId.STRUGGLE]);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.STRUGGLE);
await game.phaseInterceptor.to(TurnEndPhase);
/**
* Expect:
* - The player Pokemon (with Magic Guard) uses Struggle but does lose HP from Struggle's recoil
*/
expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp());
});
//This tests different move attributes than the recoil tests above
it("Magic Guard prevents self-damage from attacking moves", async () => {
game.override.moveset([MoveId.STEEL_BEAM]);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.STEEL_BEAM);
await game.phaseInterceptor.to(TurnEndPhase);
/**
* Expect:
* - The player Pokemon (with Magic Guard) uses a move with an HP cost but does not lose HP from using it
*/
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
});
/*
it("Magic Guard does not prevent self-damage from confusion", async () => {
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
game.move.select(MoveId.CHARM);
await game.phaseInterceptor.to(TurnEndPhase);
});
*/
it("Magic Guard does not prevent self-damage from non-attacking moves", async () => {
game.override.moveset([MoveId.BELLY_DRUM]);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.BELLY_DRUM);
await game.phaseInterceptor.to(TurnEndPhase);
/**
* Expect:
* - The player Pokemon (with Magic Guard) uses a non-attacking move with an HP cost and thus loses HP from using it
*/
expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp());
});
it("Magic Guard prevents damage from abilities with PostTurnHurtIfSleepingAbAttr", async () => {
//Tests the ability Bad Dreams
game.override.statusEffect(StatusEffect.SLEEP);
//enemy pokemon is given Spore just in case player pokemon somehow awakens during test
game.override
.enemyMoveset([MoveId.SPORE, MoveId.SPORE, MoveId.SPORE, MoveId.SPORE])
.enemyAbility(AbilityId.BAD_DREAMS);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.SPLASH);
await game.phaseInterceptor.to(TurnEndPhase);
/**
* Expect:
* - The player Pokemon (with Magic Guard) should not lose HP due to this ability attribute
* - The player Pokemon is asleep
*/
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
expect(leadPokemon.status!.effect).toBe(StatusEffect.SLEEP);
});
it("Magic Guard prevents damage from abilities with PostFaintContactDamageAbAttr", async () => {
//Tests the abilities Innards Out/Aftermath
game.override.moveset([MoveId.TACKLE]).enemyAbility(AbilityId.AFTERMATH);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
enemyPokemon.hp = 1;
game.move.select(MoveId.TACKLE);
await game.phaseInterceptor.to(TurnEndPhase);
/**
* Expect:
* - The player Pokemon (with Magic Guard) should not lose HP due to this ability attribute
* - The enemy Pokemon has fainted
*/
expect(enemyPokemon.hp).toBe(0);
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
});
it("Magic Guard prevents damage from abilities with PostDefendContactDamageAbAttr", async () => {
//Tests the abilities Iron Barbs/Rough Skin
game.override.moveset([MoveId.TACKLE]).enemyAbility(AbilityId.IRON_BARBS);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(MoveId.TACKLE);
await game.phaseInterceptor.to(TurnEndPhase);
/**
* Expect:
* - The player Pokemon (with Magic Guard) should not lose HP due to this ability attribute
* - The player Pokemon's move should have connected
*/
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
});
it("Magic Guard prevents damage from abilities with ReverseDrainAbAttr", async () => {
//Tests the ability Liquid Ooze
game.override.moveset([MoveId.ABSORB]).enemyAbility(AbilityId.LIQUID_OOZE);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(MoveId.ABSORB);
await game.phaseInterceptor.to(TurnEndPhase);
/**
* Expect:
* - The player Pokemon (with Magic Guard) should not lose HP due to this ability attribute
* - The player Pokemon's move should have connected
*/
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
});
it("Magic Guard prevents HP loss from abilities with PostWeatherLapseDamageAbAttr", async () => {
game.override.passiveAbility(AbilityId.SOLAR_POWER).weather(WeatherType.SUNNY);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.SPLASH);
await game.phaseInterceptor.to(TurnEndPhase);
/**
* Expect:
* - The player Pokemon (with Magic Guard) should not lose HP due to this ability attribute
*/
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
// Magic guard prevented damage but not poison
const player = game.field.getPlayerPokemon();
expect(player.hp).toBe(player.getMaxHp());
expect(player.status?.effect).toBe(StatusEffect.POISON);
});
});

View File

@ -26,7 +26,7 @@ import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { BiomeId } from "#enums/biome-id";
describe("Test Battle Phase", () => {
describe("Phase - Battle Phase", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
@ -197,47 +197,25 @@ describe("Test Battle Phase", () => {
await game.phaseInterceptor.runFrom(SelectGenderPhase).to(SummonPhase);
});
it("2vs1", async () => {
game.override.battleStyle("single");
game.override.enemySpecies(SpeciesId.MIGHTYENA);
game.override.enemyAbility(AbilityId.HYDRATION);
game.override.ability(AbilityId.HYDRATION);
await game.classicMode.startBattle([SpeciesId.BLASTOISE, SpeciesId.CHARIZARD]);
expect(game.scene.ui?.getMode()).toBe(UiMode.COMMAND);
expect(game.scene.phaseManager.getCurrentPhase()!.constructor.name).toBe(CommandPhase.name);
}, 20000);
it.each([
{ name: "1v1", double: false, qty: 1 },
{ name: "2v1", double: false, qty: 2 },
{ name: "2v2", double: true, qty: 2 },
{ name: "4v2", double: true, qty: 4 },
])("should not crash when starting $name battle", async ({ double, qty }) => {
game.override
.battleStyle(double ? "double" : "single")
.enemySpecies(SpeciesId.MIGHTYENA)
.enemyAbility(AbilityId.HYDRATION)
.ability(AbilityId.HYDRATION);
it("1vs1", async () => {
game.override.battleStyle("single");
game.override.enemySpecies(SpeciesId.MIGHTYENA);
game.override.enemyAbility(AbilityId.HYDRATION);
game.override.ability(AbilityId.HYDRATION);
await game.classicMode.startBattle([SpeciesId.BLASTOISE]);
expect(game.scene.ui?.getMode()).toBe(UiMode.COMMAND);
expect(game.scene.phaseManager.getCurrentPhase()!.constructor.name).toBe(CommandPhase.name);
}, 20000);
await game.classicMode.startBattle(
[SpeciesId.BLASTOISE, SpeciesId.CHARIZARD, SpeciesId.DARKRAI, SpeciesId.GABITE].slice(0, qty),
);
it("2vs2", async () => {
game.override.battleStyle("double");
game.override.enemySpecies(SpeciesId.MIGHTYENA);
game.override.enemyAbility(AbilityId.HYDRATION);
game.override.ability(AbilityId.HYDRATION);
game.override.startingWave(3);
await game.classicMode.startBattle([SpeciesId.BLASTOISE, SpeciesId.CHARIZARD]);
expect(game.scene.ui?.getMode()).toBe(UiMode.COMMAND);
expect(game.scene.phaseManager.getCurrentPhase()!.constructor.name).toBe(CommandPhase.name);
}, 20000);
it("4vs2", async () => {
game.override.battleStyle("double");
game.override.enemySpecies(SpeciesId.MIGHTYENA);
game.override.enemyAbility(AbilityId.HYDRATION);
game.override.ability(AbilityId.HYDRATION);
game.override.startingWave(3);
await game.classicMode.startBattle([SpeciesId.BLASTOISE, SpeciesId.CHARIZARD, SpeciesId.DARKRAI, SpeciesId.GABITE]);
expect(game.scene.ui?.getMode()).toBe(UiMode.COMMAND);
expect(game.scene.phaseManager.getCurrentPhase()!.constructor.name).toBe(CommandPhase.name);
}, 20000);
expect(game.scene.phaseManager.getCurrentPhase()).toBeInstanceOf(CommandPhase);
});
it("kill opponent pokemon", async () => {
const moveToUse = MoveId.SPLASH;

View File

@ -209,9 +209,9 @@ export class OverridesHelper extends GameManagerHelper {
}
/**
* Override the player pokemon's {@linkcode StatusEffect | status-effect}
* Override the player pokemon's initial {@linkcode StatusEffect | status-effect},
* @param statusEffect - The {@linkcode StatusEffect | status-effect} to set
* @returns
* @returns `this`
*/
public statusEffect(statusEffect: StatusEffect): this {
vi.spyOn(Overrides, "STATUS_OVERRIDE", "get").mockReturnValue(statusEffect);
@ -401,9 +401,9 @@ export class OverridesHelper extends GameManagerHelper {
}
/**
* Override the enemy {@linkcode StatusEffect | status-effect} for enemy pokemon
* Override the enemy pokemon's initial {@linkcode StatusEffect | status-effect}.
* @param statusEffect - The {@linkcode StatusEffect | status-effect} to set
* @returns
* @returns `this`
*/
public enemyStatusEffect(statusEffect: StatusEffect): this {
vi.spyOn(Overrides, "OPP_STATUS_OVERRIDE", "get").mockReturnValue(statusEffect);