Implement Safeguard for non-volatile statuses

This commit is contained in:
NightKev 2024-08-08 16:44:46 -07:00
parent 38303e6eb1
commit b5f948ffec
4 changed files with 82 additions and 8 deletions

View File

@ -845,7 +845,7 @@ export class PostDefendTerrainChangeAbAttr extends PostDefendAbAttr {
} }
export class PostDefendContactApplyStatusEffectAbAttr extends PostDefendAbAttr { export class PostDefendContactApplyStatusEffectAbAttr extends PostDefendAbAttr {
private chance: integer; public chance: integer;
private effects: StatusEffect[]; private effects: StatusEffect[];
constructor(chance: integer, ...effects: StatusEffect[]) { constructor(chance: integer, ...effects: StatusEffect[]) {

View File

@ -1929,7 +1929,7 @@ export class StatusEffectAttr extends MoveEffectAttr {
} }
} }
if (user.scene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, targetSide)) { if (user !== target && user.scene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, targetSide)) {
if (move.category === MoveCategory.STATUS) { if (move.category === MoveCategory.STATUS) {
user.scene.queueMessage(i18next.t("moveTriggers:safeguard", { targetName: getPokemonNameWithAffix(target)})); user.scene.queueMessage(i18next.t("moveTriggers:safeguard", { targetName: getPokemonNameWithAffix(target)}));
} }
@ -6748,7 +6748,8 @@ export function initMoves() {
.attr(FriendshipPowerAttr, true), .attr(FriendshipPowerAttr, true),
new StatusMove(Moves.SAFEGUARD, Type.NORMAL, -1, 25, -1, 0, 2) new StatusMove(Moves.SAFEGUARD, Type.NORMAL, -1, 25, -1, 0, 2)
.target(MoveTarget.USER_SIDE) .target(MoveTarget.USER_SIDE)
.attr(AddArenaTagAttr, ArenaTagType.SAFEGUARD, 5, true, true), .attr(AddArenaTagAttr, ArenaTagType.SAFEGUARD, 5, true, true)
.partial(),
new StatusMove(Moves.PAIN_SPLIT, Type.NORMAL, -1, 20, -1, 0, 2) new StatusMove(Moves.PAIN_SPLIT, Type.NORMAL, -1, 20, -1, 0, 2)
.attr(HpSplitAttr) .attr(HpSplitAttr)
.condition(failOnBossCondition), .condition(failOnBossCondition),

View File

@ -2686,6 +2686,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const types = this.getTypes(true, true); const types = this.getTypes(true, true);
const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
if (sourcePokemon && sourcePokemon !== this && this.scene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, defendingSide)) {
return false;
}
switch (effect) { switch (effect) {
case StatusEffect.POISON: case StatusEffect.POISON:
case StatusEffect.TOXIC: case StatusEffect.TOXIC:

View File

@ -1,4 +1,4 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import Phaser from "phaser"; import Phaser from "phaser";
import GameManager from "#app/test/utils/gameManager"; import GameManager from "#app/test/utils/gameManager";
import { CommandPhase, SelectTargetPhase, TurnEndPhase } from "#app/phases"; import { CommandPhase, SelectTargetPhase, TurnEndPhase } from "#app/phases";
@ -8,6 +8,8 @@ import { Species } from "#enums/species";
import { BattlerIndex } from "#app/battle.js"; import { BattlerIndex } from "#app/battle.js";
import { Abilities } from "#app/enums/abilities.js"; import { Abilities } from "#app/enums/abilities.js";
import { mockTurnOrder } from "../utils/testUtils"; import { mockTurnOrder } from "../utils/testUtils";
import { StatusEffect } from "#app/enums/status-effect.js";
import { allAbilities, PostDefendContactApplyStatusEffectAbAttr } from "#app/data/ability.js";
const TIMEOUT = 20 * 1000; const TIMEOUT = 20 * 1000;
@ -34,10 +36,10 @@ describe("Moves - Safeguard", () => {
.enemyAbility(Abilities.BALL_FETCH) .enemyAbility(Abilities.BALL_FETCH)
.enemyLevel(5) .enemyLevel(5)
.starterSpecies(Species.DRATINI) .starterSpecies(Species.DRATINI)
.moveset([Moves.NUZZLE, Moves.SPORE]) .moveset([Moves.NUZZLE, Moves.SPORE, Moves.YAWN, Moves.SPLASH])
.ability(Abilities.BALL_FETCH); .ability(Abilities.BALL_FETCH);
}); });
it("protects from nuzzle status", it("protects from damaging moves with additional effects",
async () => { async () => {
await game.startBattle(); await game.startBattle();
const enemy = game.scene.getEnemyPokemon()!; const enemy = game.scene.getEnemyPokemon()!;
@ -49,9 +51,8 @@ describe("Moves - Safeguard", () => {
expect(enemy.status).toBeUndefined(); expect(enemy.status).toBeUndefined();
}, TIMEOUT }, TIMEOUT
); );
it("protects from spore", it("protects from status moves",
async () => { async () => {
await game.startBattle(); await game.startBattle();
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
@ -88,4 +89,71 @@ describe("Moves - Safeguard", () => {
expect(enemyPokemon[1].status).toBeUndefined(); expect(enemyPokemon[1].status).toBeUndefined();
}, TIMEOUT }, TIMEOUT
); );
it.skip("protects from new volatile status", // not yet
async () => {
await game.startBattle();
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.doAttack(getMovePosition(game.scene, 0, Moves.YAWN));
await mockTurnOrder(game, [BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.toNextTurn();
expect(enemyPokemon.summonData.tags).toEqual([]);
}, TIMEOUT
);
it.skip("doesn't protect from already existing volatile status", // not yet
async () => {
await game.startBattle();
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.doAttack(getMovePosition(game.scene, 0, Moves.YAWN));
await mockTurnOrder(game, [BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.toNextTurn();
game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH));
await game.toNextTurn();
expect(enemyPokemon.status?.effect).toEqual(StatusEffect.SLEEP);
}, TIMEOUT
);
it("doesn't protect from self-inflicted via Rest or Flame Orb",
async () => {
game.override.enemyHeldItems([{name: "FLAME_ORB"}]);
await game.startBattle();
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH));
await mockTurnOrder(game, [BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.toNextTurn();
expect(enemyPokemon.status?.effect).toEqual(StatusEffect.BURN);
game.override.enemyMoveset(Array(4).fill(Moves.REST));
game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH));
await game.toNextTurn();
expect(enemyPokemon.status?.effect).toEqual(StatusEffect.SLEEP);
}, TIMEOUT
);
it("protects from ability-inflicted status",
async () => {
game.override.ability(Abilities.STATIC);
vi.spyOn(allAbilities[Abilities.STATIC].getAttrs(PostDefendContactApplyStatusEffectAbAttr)[0], "chance", "get").mockReturnValue(100);
await game.startBattle();
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH));
await mockTurnOrder(game, [BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.toNextTurn();
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH));
await game.toNextTurn();
expect(enemyPokemon.status).toBeUndefined();
}, TIMEOUT
);
}); });