[Ability] Fully implement Pastel Veil, update Sweet Veil (after beta fix) (#3208)

* ful implement pastel veil, update sweet veil

* improve docs

* update docs

* cleanup attrs
This commit is contained in:
Adrian T. 2024-07-30 22:06:31 +08:00 committed by GitHub
parent 208f5af62a
commit 5b4a24824f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 297 additions and 8 deletions

View File

@ -1,4 +1,4 @@
import Pokemon, { HitResult, PokemonMove } from "../field/pokemon";
import Pokemon, { HitResult, PlayerPokemon, PokemonMove } from "../field/pokemon";
import { Type } from "./type";
import { Constructor } from "#app/utils";
import * as Utils from "../utils";
@ -2147,6 +2147,49 @@ export class PostSummonCopyAbilityAbAttr extends PostSummonAbAttr {
}
}
/**
* Removes supplied status effects from the user's field.
*/
export class PostSummonUserFieldRemoveStatusEffectAbAttr extends PostSummonAbAttr {
private statusEffect: StatusEffect[];
/**
* @param statusEffect - The status effects to be removed from the user's field.
*/
constructor(...statusEffect: StatusEffect[]) {
super(false);
this.statusEffect = statusEffect;
}
/**
* Removes supplied status effect from the user's field when user of the ability is summoned.
*
* @param pokemon - The Pokémon that triggered the ability.
* @param passive - n/a
* @param args - n/a
* @returns A boolean or a promise that resolves to a boolean indicating the result of the ability application.
*/
applyPostSummon(pokemon: Pokemon, passive: boolean, args: any[]): boolean | Promise<boolean> {
const party = pokemon instanceof PlayerPokemon ? pokemon.scene.getPlayerField() : pokemon.scene.getEnemyField();
const allowedParty = party.filter(p => p.isAllowedInBattle());
if (allowedParty.length < 1) {
return false;
}
for (const pokemon of allowedParty) {
if (this.statusEffect.includes(pokemon.status?.effect)) {
pokemon.scene.queueMessage(getStatusEffectHealText(pokemon.status.effect, getPokemonNameWithAffix(pokemon)));
pokemon.resetStatus(false);
pokemon.updateInfo();
}
}
return true;
}
}
/** Attempt to copy the stat changes on an ally pokemon */
export class PostSummonCopyAllyStatsAbAttr extends PostSummonAbAttr {
@ -2398,17 +2441,33 @@ export class PreSetStatusAbAttr extends AbAttr {
}
}
export class StatusEffectImmunityAbAttr extends PreSetStatusAbAttr {
/**
* Provides immunity to status effects to specified targets.
*/
export class PreSetStatusEffectImmunityAbAttr extends PreSetStatusAbAttr {
private immuneEffects: StatusEffect[];
/**
* @param immuneEffects - The status effects to which the Pokémon is immune.
*/
constructor(...immuneEffects: StatusEffect[]) {
super();
this.immuneEffects = immuneEffects;
}
/**
* Applies immunity to supplied status effects.
*
* @param pokemon - The Pokémon to which the status is being applied.
* @param passive - n/a
* @param effect - The status effect being applied.
* @param cancelled - A holder for a boolean value indicating if the status application was cancelled.
* @param args - n/a
* @returns A boolean indicating the result of the status application.
*/
applyPreSetStatus(pokemon: Pokemon, passive: boolean, effect: StatusEffect, cancelled: Utils.BooleanHolder, args: any[]): boolean {
if (!this.immuneEffects.length || this.immuneEffects.indexOf(effect) > -1) {
if (this.immuneEffects.length < 1 || this.immuneEffects.includes(effect)) {
cancelled.value = true;
return true;
}
@ -2430,13 +2489,28 @@ export class StatusEffectImmunityAbAttr extends PreSetStatusAbAttr {
}
}
/**
* Provides immunity to status effects to the user.
* @extends PreSetStatusEffectImmunityAbAttr
*/
export class StatusEffectImmunityAbAttr extends PreSetStatusEffectImmunityAbAttr { }
/**
* Provides immunity to status effects to the user's field.
* @extends PreSetStatusEffectImmunityAbAttr
*/
export class UserFieldStatusEffectImmunityAbAttr extends PreSetStatusEffectImmunityAbAttr { }
export class PreApplyBattlerTagAbAttr extends AbAttr {
applyPreApplyBattlerTag(pokemon: Pokemon, passive: boolean, tag: BattlerTag, cancelled: Utils.BooleanHolder, args: any[]): boolean | Promise<boolean> {
return false;
}
}
export class BattlerTagImmunityAbAttr extends PreApplyBattlerTagAbAttr {
/**
* Provides immunity to BattlerTags {@linkcode BattlerTag} to specified targets.
*/
export class PreApplyBattlerTagImmunityAbAttr extends PreApplyBattlerTagAbAttr {
private immuneTagType: BattlerTagType;
constructor(immuneTagType: BattlerTagType) {
@ -2463,6 +2537,18 @@ export class BattlerTagImmunityAbAttr extends PreApplyBattlerTagAbAttr {
}
}
/**
* Provides immunity to BattlerTags {@linkcode BattlerTag} to the user.
* @extends PreApplyBattlerTagImmunityAbAttr
*/
export class BattlerTagImmunityAbAttr extends PreApplyBattlerTagImmunityAbAttr { }
/**
* Provides immunity to BattlerTags {@linkcode BattlerTag} to the user's field.
* @extends PreApplyBattlerTagImmunityAbAttr
*/
export class UserFieldBattlerTagImmunityAbAttr extends PreApplyBattlerTagImmunityAbAttr { }
export class BlockCritAbAttr extends AbAttr {
apply(pokemon: Pokemon, passive: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean {
(args[0] as Utils.BooleanHolder).value = true;
@ -4722,10 +4808,10 @@ export function initAbilities() {
new Ability(Abilities.REFRIGERATE, 6)
.attr(MoveTypeChangeAttr, Type.ICE, 1.2, (user, target, move) => move.type === Type.NORMAL),
new Ability(Abilities.SWEET_VEIL, 6)
.attr(StatusEffectImmunityAbAttr, StatusEffect.SLEEP)
.attr(UserFieldStatusEffectImmunityAbAttr, StatusEffect.SLEEP)
.attr(BattlerTagImmunityAbAttr, BattlerTagType.DROWSY)
.ignorable()
.partial(),
.partial(), // Mold Breaker ally should not be affected by Sweet Veil
new Ability(Abilities.STANCE_CHANGE, 6)
.attr(UncopiableAbilityAbAttr)
.attr(UnswappableAbilityAbAttr)
@ -5020,7 +5106,8 @@ export function initAbilities() {
.attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonNeutralizingGas", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }))
.partial(),
new Ability(Abilities.PASTEL_VEIL, 8)
.attr(StatusEffectImmunityAbAttr, StatusEffect.POISON, StatusEffect.TOXIC)
.attr(PostSummonUserFieldRemoveStatusEffectAbAttr, StatusEffect.POISON, StatusEffect.TOXIC)
.attr(UserFieldStatusEffectImmunityAbAttr, StatusEffect.POISON, StatusEffect.TOXIC)
.ignorable(),
new Ability(Abilities.HUNGER_SWITCH, 8)
.attr(PostTurnFormChangeAbAttr, p => p.getFormKey ? 0 : 1)

View File

@ -23,7 +23,7 @@ import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoo
import { WeatherType } from "../data/weather";
import { TempBattleStat } from "../data/temp-battle-stat";
import { ArenaTagSide, WeakenMoveScreenTag } from "../data/arena-tag";
import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, PreApplyBattlerTagAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AddSecondStrikeAbAttr, IgnoreOpponentEvasionAbAttr } from "../data/ability";
import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, PreApplyBattlerTagAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AddSecondStrikeAbAttr, IgnoreOpponentEvasionAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr } from "../data/ability";
import PokemonData from "../system/pokemon-data";
import { BattlerIndex } from "../battle";
import { Mode } from "../ui/ui";
@ -1779,6 +1779,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return (this.isPlayer() ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.getFieldIndex() ? 0 : 1];
}
/**
* Gets the Pokémon on the allied field.
*
* @returns An array of Pokémon on the allied field.
*/
getAlliedField(): Pokemon[] {
return this instanceof PlayerPokemon ? this.scene.getPlayerField() : this.scene.getEnemyField();
}
/**
* Calculates the accuracy multiplier of the user against a target.
*
@ -2236,6 +2245,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const cancelled = new Utils.BooleanHolder(false);
applyPreApplyBattlerTagAbAttrs(PreApplyBattlerTagAbAttr, this, newTag, cancelled);
const userField = this.getAlliedField();
userField.forEach(pokemon => applyPreApplyBattlerTagAbAttrs(UserFieldBattlerTagImmunityAbAttr, pokemon, newTag, cancelled));
if (!cancelled.value && newTag.canAdd(this)) {
this.summonData.tags.push(newTag);
newTag.onAdd(this);
@ -2644,6 +2656,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const cancelled = new Utils.BooleanHolder(false);
applyPreSetStatusAbAttrs(StatusEffectImmunityAbAttr, this, effect, cancelled, quiet);
const userField = this.getAlliedField();
userField.forEach(pokemon => applyPreSetStatusAbAttrs(UserFieldStatusEffectImmunityAbAttr, pokemon, effect, cancelled, quiet));
if (cancelled.value) {
return false;
}

View File

@ -0,0 +1,79 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import Phaser from "phaser";
import GameManager from "#app/test/utils/gameManager";
import overrides from "#app/overrides";
import { Species } from "#enums/species";
import {
CommandPhase,
TurnEndPhase,
} from "#app/phases";
import { Moves } from "#enums/moves";
import { getMovePosition } from "#app/test/utils/gameManagerUtils";
import { StatusEffect } from "#app/data/status-effect.js";
import { allAbilities } from "#app/data/ability.js";
import { Abilities } from "#app/enums/abilities.js";
import { BattlerIndex } from "#app/battle.js";
describe("Abilities - Pastel Veil", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
vi.spyOn(overrides, "BATTLE_TYPE_OVERRIDE", "get").mockReturnValue("double");
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH]);
vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.BALL_FETCH);
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MAGIKARP);
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TOXIC_THREAD, Moves.TOXIC_THREAD, Moves.TOXIC_THREAD, Moves.TOXIC_THREAD]);
});
it("prevents the user and its allies from being afflicted by poison", async () => {
await game.startBattle([Species.GALAR_PONYTA, Species.MAGIKARP]);
const ponyta = game.scene.getPlayerField()[0];
vi.spyOn(ponyta, "getAbility").mockReturnValue(allAbilities[Abilities.PASTEL_VEIL]);
expect(ponyta.hasAbility(Abilities.PASTEL_VEIL)).toBe(true);
game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH));
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
await game.phaseInterceptor.to(TurnEndPhase);
expect(game.scene.getPlayerField().every(p => p.status?.effect)).toBe(false);
});
it("it heals the poisoned status condition of allies if user is sent out into battle", async () => {
await game.startBattle([Species.MAGIKARP, Species.MAGIKARP, Species.GALAR_PONYTA]);
const ponyta = game.scene.getParty().find(p => p.species.speciesId === Species.GALAR_PONYTA);
vi.spyOn(ponyta, "getAbility").mockReturnValue(allAbilities[Abilities.PASTEL_VEIL]);
expect(ponyta.hasAbility(Abilities.PASTEL_VEIL)).toBe(true);
game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH));
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
await game.phaseInterceptor.to(TurnEndPhase);
expect(game.scene.getPlayerField().some(p => p.status?.effect === StatusEffect.POISON)).toBe(true);
const poisonedMon = game.scene.getPlayerField().find(p => p.status?.effect === StatusEffect.POISON);
await game.phaseInterceptor.to(CommandPhase);
game.doAttack(getMovePosition(game.scene, (poisonedMon.getBattlerIndex() as BattlerIndex.PLAYER | BattlerIndex.PLAYER_2), Moves.SPLASH));
game.doSwitchPokemon(2);
await game.phaseInterceptor.to(TurnEndPhase);
expect(game.scene.getPlayerField().every(p => p.status?.effect)).toBe(false);
});
});

View File

@ -0,0 +1,108 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import Phaser from "phaser";
import GameManager from "#app/test/utils/gameManager";
import overrides from "#app/overrides";
import { Species } from "#enums/species";
import {
CommandPhase,
MoveEffectPhase,
MovePhase,
TurnEndPhase,
} from "#app/phases";
import { Moves } from "#enums/moves";
import { getMovePosition } from "#app/test/utils/gameManagerUtils";
import { BattlerTagType } from "#app/enums/battler-tag-type.js";
import { Abilities } from "#app/enums/abilities.js";
import { BattlerIndex } from "#app/battle.js";
describe("Abilities - Sweet Veil", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
vi.spyOn(overrides, "BATTLE_TYPE_OVERRIDE", "get").mockReturnValue("double");
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.REST]);
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MAGIKARP);
vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.BALL_FETCH);
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.POWDER, Moves.POWDER, Moves.POWDER, Moves.POWDER]);
});
it("prevents the user and its allies from falling asleep", async () => {
await game.startBattle([Species.SWIRLIX, Species.MAGIKARP]);
game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH));
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
await game.phaseInterceptor.to(TurnEndPhase);
expect(game.scene.getPlayerField().every(p => p.status?.effect)).toBe(false);
});
it("causes Rest to fail when used by the user or its allies", async () => {
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]);
await game.startBattle([Species.SWIRLIX, Species.MAGIKARP]);
game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH));
game.doAttack(getMovePosition(game.scene, 1, Moves.REST));
await game.phaseInterceptor.to(TurnEndPhase);
expect(game.scene.getPlayerField().every(p => p.status?.effect)).toBe(false);
});
it("causes Yawn to fail if used on the user or its allies", async () => {
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.YAWN, Moves.YAWN, Moves.YAWN, Moves.YAWN]);
await game.startBattle([Species.SWIRLIX, Species.MAGIKARP]);
game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH));
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
await game.phaseInterceptor.to(TurnEndPhase);
expect(game.scene.getPlayerField().every(p => !!p.getTag(BattlerTagType.DROWSY))).toBe(false);
});
it("prevents the user and its allies already drowsy due to Yawn from falling asleep.", async () => {
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.PIKACHU);
vi.spyOn(overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(5);
vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(5);
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.YAWN, Moves.YAWN, Moves.YAWN, Moves.YAWN]);
await game.startBattle([Species.SHUCKLE, Species.SHUCKLE, Species.SWIRLIX]);
game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH));
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
// First pokemon move
await game.phaseInterceptor.to(MoveEffectPhase, false);
vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValueOnce(true);
// Second pokemon move
await game.phaseInterceptor.to(MovePhase, false);
await game.phaseInterceptor.to(MoveEffectPhase, false);
vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValueOnce(true);
expect(game.scene.getPlayerField().some(p => !!p.getTag(BattlerTagType.DROWSY))).toBe(true);
await game.phaseInterceptor.to(TurnEndPhase);
const drowsyMon = game.scene.getPlayerField().find(p => !!p.getTag(BattlerTagType.DROWSY));
await game.phaseInterceptor.to(CommandPhase);
game.doAttack(getMovePosition(game.scene, (drowsyMon.getBattlerIndex() as BattlerIndex.PLAYER | BattlerIndex.PLAYER_2), Moves.SPLASH));
game.doSwitchPokemon(2);
expect(game.scene.getPlayerField().every(p => p.status?.effect)).toBe(false);
});
});