This commit is contained in:
Bertie690 2025-06-20 10:03:53 -04:00 committed by GitHub
commit 65b036fccd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 235 additions and 434 deletions

View File

@ -6535,13 +6535,26 @@ export class PostFaintContactDamageAbAttr extends PostFaintAbAttr {
_hitResult?: HitResult, _hitResult?: HitResult,
..._args: any[] ..._args: any[]
): boolean { ): boolean {
const diedToDirectDamage = // Return early if ability user did not die to a direct-contact attack.
move !== undefined && if (
attacker !== undefined && move === undefined ||
move.doesFlagEffectApply({ flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon }); attacker === undefined ||
!move.doesFlagEffectApply({ flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon })
) {
return false;
}
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
globalScene.getField(true).map(p => applyAbAttrs("FieldPreventExplosiveMovesAbAttr", p, cancelled, simulated)); // TODO: This should be in speed order
return !(!diedToDirectDamage || cancelled.value || attacker!.hasAbilityWithAttr("BlockNonDirectDamageAbAttr")); 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( override applyPostFaint(
@ -6550,15 +6563,15 @@ export class PostFaintContactDamageAbAttr extends PostFaintAbAttr {
simulated: boolean, simulated: boolean,
attacker?: Pokemon, attacker?: Pokemon,
_move?: Move, _move?: Move,
_hitResult?: HitResult,
..._args: any[]
): void { ): void {
if (!simulated) { if (simulated) {
attacker!.damageAndUpdate(toDmgValue(attacker!.getMaxHp() * (1 / this.damageRatio)), { return;
result: HitResult.INDIRECT,
});
attacker!.turnData.damageTaken += toDmgValue(attacker!.getMaxHp() * (1 / this.damageRatio));
} }
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 { getTriggerMessage(pokemon: Pokemon, abilityName: string, ..._args: any[]): string {
@ -6570,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 { export class PostFaintHPDamageAbAttr extends PostFaintAbAttr {
override applyPostFaint( override applyPostFaint(
pokemon: Pokemon, pokemon: Pokemon,
_passive: boolean, _passive: boolean,
simulated: boolean, _simulated: boolean,
attacker?: Pokemon, attacker?: Pokemon,
move?: Move, move?: Move,
_hitResult?: HitResult,
..._args: any[]
): void { ): void {
//If the mon didn't die to indirect 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 && !simulated) { if (move === undefined || attacker === undefined || attacker.getAlly() === pokemon) {
const damage = pokemon.turnData.attacksReceived[0].damage; return;
attacker.damageAndUpdate(damage, { result: HitResult.INDIRECT });
attacker.turnData.damageTaken += damage;
} }
// 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", { return i18next.t("abilityTriggers:postFaintHpDamage", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
abilityName, 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 { ArenaTagSide } from "#enums/arena-tag-side";
import { allMoves } from "#app/data/data-lists";
import { getStatusEffectCatchRateMultiplier } from "#app/data/status-effect"; 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 { AbilityId } from "#enums/ability-id";
import { ArenaTagType } from "#enums/arena-tag-type"; import { ArenaTagType } from "#enums/arena-tag-type";
import { BattlerTagType } from "#enums/battler-tag-type";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
import { WeatherType } from "#enums/weather-type";
import GameManager from "#test/testUtils/gameManager"; import GameManager from "#test/testUtils/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, vi } from "vitest";
describe("Abilities - Magic Guard", () => { describe("AbilityId - Magic Guard", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
let game: GameManager; let game: GameManager;
@ -29,404 +28,142 @@ describe("Abilities - Magic Guard", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override game.override
/** Player Pokemon overrides */
.ability(AbilityId.MAGIC_GUARD) .ability(AbilityId.MAGIC_GUARD)
.moveset([MoveId.SPLASH]) .enemySpecies(SpeciesId.BLISSEY)
.enemyAbility(AbilityId.NO_GUARD)
.startingLevel(100) .startingLevel(100)
/** Enemy Pokemon overrides */
.enemySpecies(SpeciesId.SNORLAX)
.enemyAbility(AbilityId.INSOMNIA)
.enemyMoveset(MoveId.SPLASH)
.enemyLevel(100); .enemyLevel(100);
}); });
//Bulbapedia Reference: https://bulbapedia.bulbagarden.net/wiki/Magic_Guard_(Ability) //Bulbapedia Reference: https://bulbapedia.bulbagarden.net/wiki/Magic_Guard_(Ability)
it("ability should prevent damage caused by weather", async () => { it.each<{ name: string; move?: MoveId; enemyMove?: MoveId }>([
game.override.weather(WeatherType.SANDSTORM); { 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]); 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()!; const magikarp = game.field.getPlayerPokemon();
expect(enemyPokemon).toBeDefined(); expect(magikarp.hp).toBe(magikarp.getMaxHp());
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());
}); });
it("ability should prevent damage caused by status effects but other non-damage effects still apply", async () => { it.each<{ abName: string; move?: MoveId; enemyMove?: MoveId; passive?: AbilityId; enemyAbility?: AbilityId }>([
//Toxic keeps track of the turn counters -> important that Magic Guard keeps track of post-Toxic turns { abName: "Bad Dreams", enemyMove: MoveId.SPORE, enemyAbility: AbilityId.BAD_DREAMS },
game.override.statusEffect(StatusEffect.POISON); { 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]); 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); const leadPokemon = game.field.getPlayerPokemon();
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
*/
expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp()); expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp());
}); });
it("Magic Guard prevents damage caused by burn but other non-damaging effects are still applied", async () => { it("should preserve toxic turn count and deal appropriate damage when disabled", async () => {
game.override.enemyStatusEffect(StatusEffect.BURN).enemyAbility(AbilityId.MAGIC_GUARD); game.override.statusEffect(StatusEffect.TOXIC);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); 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);
/** game.move.use(MoveId.SPLASH);
* Expect: await game.move.forceEnemyMove(MoveId.GASTRO_ACID);
* - The enemy Pokemon (with Magic Guard) has not taken damage from burn await game.toNextTurn();
* - The enemy Pokemon's physical attack damage is halved (TBD)
* - The enemy Pokemon's hypothetical CatchRateMultiplier should be 1.5 expect(magikarp.status?.toxicTurnCount).toBe(5);
*/ expect(magikarp.getHpRatio(true)).toBeCloseTo(11 / 16, 1);
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
expect(getStatusEffectCatchRateMultiplier(enemyPokemon.status!.effect)).toBe(1.5);
}); });
it("Magic Guard prevents damage caused by toxic but other non-damaging effects are still applied", async () => { it("should preserve burn physical damage halving & status catch boost", async () => {
game.override.enemyStatusEffect(StatusEffect.TOXIC).enemyAbility(AbilityId.MAGIC_GUARD);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); 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; const blissey = game.field.getEnemyPokemon();
//should be 0 const prevDmg = blissey.getInverseHp();
blissey.hp = blissey.getMaxHp();
await game.phaseInterceptor.to(TurnEndPhase); expect(getStatusEffectCatchRateMultiplier(magikarp.status!.effect)).toBe(1.5);
/** game.move.use(MoveId.TACKLE);
* Expect: await game.toNextTurn();
* - The enemy Pokemon (with Magic Guard) has not taken damage from toxic
* - The enemy Pokemon's status effect duration should be incremented const burntDmg = blissey.getInverseHp();
* - The enemy Pokemon's hypothetical CatchRateMultiplier should be 1.5 expect(burntDmg).toBeCloseTo(toDmgValue(prevDmg / 2), 0);
*/
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
expect(enemyPokemon.status!.toxicTurnCount).toBeGreaterThan(toxicStartCounter);
expect(getStatusEffectCatchRateMultiplier(enemyPokemon.status!.effect)).toBe(1.5);
}); });
it("Magic Guard prevents damage caused by entry hazards", async () => { it("should prevent damage from entry hazards, but not Toxic Spikes poison", async () => {
//Adds and applies Spikes to both sides of the arena game.scene.arena.addTag(ArenaTagType.SPIKES, -1, MoveId.SPIKES, 0, ArenaTagSide.PLAYER);
const newTag = getArenaTag(ArenaTagType.SPIKES, 5, MoveId.SPIKES, 0, 0, ArenaTagSide.BOTH)!; game.scene.arena.addTag(ArenaTagType.TOXIC_SPIKES, -1, MoveId.TOXIC_SPIKES, 0, ArenaTagSide.PLAYER);
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]);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!; // Magic guard prevented damage but not poison
const player = game.field.getPlayerPokemon();
game.move.select(MoveId.HIGH_JUMP_KICK); expect(player.hp).toBe(player.getMaxHp());
await game.move.forceMiss(); expect(player.status?.effect).toBe(StatusEffect.POISON);
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());
}); });
}); });

View File

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