[Beta][P2 Bug] Fix Sappy Seed applying its secondary effect against targets with Substitute (#4430)

* Fix Sappy Seed applying Leech Seed through Substitutes

* Add docs
This commit is contained in:
innerthunder 2024-09-25 19:32:20 -07:00 committed by GitHub
parent 0500e34f87
commit 029d26b4c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 68 additions and 22 deletions

View File

@ -4586,6 +4586,30 @@ export class AddBattlerTagAttr extends MoveEffectAttr {
}
}
/**
* Adds a {@link https://bulbapedia.bulbagarden.net/wiki/Seeding | Seeding} effect to the target
* as seen with Leech Seed and Sappy Seed.
* @extends AddBattlerTagAttr
*/
export class LeechSeedAttr extends AddBattlerTagAttr {
constructor() {
super(BattlerTagType.SEEDED);
}
/**
* Adds a Seeding effect to the target if the target does not have an active Substitute.
* @param user the {@linkcode Pokemon} using the move
* @param target the {@linkcode Pokemon} targeted by the move
* @param move the {@linkcode Move} invoking this effect
* @param args n/a
* @returns `true` if the effect successfully applies; `false` otherwise
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
return !move.hitsSubstitute(user, target)
&& super.apply(user, target, move, args);
}
}
/**
* Adds the appropriate battler tag for Gulp Missile when Surf or Dive is used.
* @extends MoveEffectAttr
@ -6937,7 +6961,7 @@ export function initMoves() {
.attr(HitHealAttr)
.triageMove(),
new StatusMove(Moves.LEECH_SEED, Type.GRASS, 90, 10, -1, 0, 1)
.attr(AddBattlerTagAttr, BattlerTagType.SEEDED)
.attr(LeechSeedAttr)
.condition((user, target, move) => !target.getTag(BattlerTagType.SEEDED) && !target.isOfType(Type.GRASS)),
new SelfStatusMove(Moves.GROWTH, Type.NORMAL, -1, 20, -1, 0, 1)
.attr(GrowthStatStageChangeAttr),
@ -8921,8 +8945,8 @@ export function initMoves() {
new AttackMove(Moves.BADDY_BAD, Type.DARK, MoveCategory.SPECIAL, 80, 95, 15, -1, 0, 7)
.attr(AddArenaTagAttr, ArenaTagType.REFLECT, 5, false, true),
new AttackMove(Moves.SAPPY_SEED, Type.GRASS, MoveCategory.PHYSICAL, 100, 90, 10, 100, 0, 7)
.makesContact(false)
.attr(AddBattlerTagAttr, BattlerTagType.SEEDED),
.attr(LeechSeedAttr)
.makesContact(false),
new AttackMove(Moves.FREEZY_FROST, Type.ICE, MoveCategory.SPECIAL, 100, 90, 10, -1, 0, 7)
.attr(ResetStatsAttr, true),
new AttackMove(Moves.SPARKLY_SWIRL, Type.FAIRY, MoveCategory.SPECIAL, 120, 85, 5, -1, 0, 7)

View File

@ -1,3 +1,4 @@
import { BattlerIndex } from "#app/battle";
import { SubstituteTag, TrappedTag } from "#app/data/battler-tags";
import { allMoves, StealHeldItemChanceAttr } from "#app/data/move";
import { StatusEffect } from "#app/data/status-effect";
@ -61,7 +62,7 @@ describe("Moves - Substitute", () => {
it(
"should redirect enemy attack damage to the Substitute doll",
async () => {
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
game.override.enemyMoveset(Moves.TACKLE);
await game.classicMode.startBattle([Species.SKARMORY]);
@ -86,7 +87,7 @@ describe("Moves - Substitute", () => {
"should fade after redirecting more damage than its remaining HP",
async () => {
// Giga Impact OHKOs Magikarp if substitute isn't up
game.override.enemyMoveset(Array(4).fill(Moves.GIGA_IMPACT));
game.override.enemyMoveset(Moves.GIGA_IMPACT);
vi.spyOn(allMoves[Moves.GIGA_IMPACT], "accuracy", "get").mockReturnValue(100);
await game.classicMode.startBattle([Species.MAGIKARP]);
@ -111,7 +112,7 @@ describe("Moves - Substitute", () => {
it(
"should block stat changes from status moves",
async () => {
game.override.enemyMoveset(Array(4).fill(Moves.CHARM));
game.override.enemyMoveset(Moves.CHARM);
await game.classicMode.startBattle([Species.MAGIKARP]);
@ -129,7 +130,7 @@ describe("Moves - Substitute", () => {
it(
"should be bypassed by sound-based moves",
async () => {
game.override.enemyMoveset(Array(4).fill(Moves.ECHOED_VOICE));
game.override.enemyMoveset(Moves.ECHOED_VOICE);
await game.classicMode.startBattle([Species.BLASTOISE]);
@ -152,7 +153,7 @@ describe("Moves - Substitute", () => {
it(
"should be bypassed by attackers with Infiltrator",
async () => {
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
game.override.enemyMoveset(Moves.TACKLE);
game.override.enemyAbility(Abilities.INFILTRATOR);
await game.classicMode.startBattle([Species.BLASTOISE]);
@ -196,7 +197,7 @@ describe("Moves - Substitute", () => {
it(
"should protect the user from flinching",
async () => {
game.override.enemyMoveset(Array(4).fill(Moves.FAKE_OUT));
game.override.enemyMoveset(Moves.FAKE_OUT);
game.override.startingLevel(1); // Ensures the Substitute will break
await game.classicMode.startBattle([Species.BLASTOISE]);
@ -218,7 +219,7 @@ describe("Moves - Substitute", () => {
"should protect the user from being trapped",
async () => {
vi.spyOn(allMoves[Moves.SAND_TOMB], "accuracy", "get").mockReturnValue(100);
game.override.enemyMoveset(Array(4).fill(Moves.SAND_TOMB));
game.override.enemyMoveset(Moves.SAND_TOMB);
await game.classicMode.startBattle([Species.BLASTOISE]);
@ -238,7 +239,7 @@ describe("Moves - Substitute", () => {
"should prevent the user's stats from being lowered",
async () => {
vi.spyOn(allMoves[Moves.LIQUIDATION], "chance", "get").mockReturnValue(100);
game.override.enemyMoveset(Array(4).fill(Moves.LIQUIDATION));
game.override.enemyMoveset(Moves.LIQUIDATION);
await game.classicMode.startBattle([Species.BLASTOISE]);
@ -257,7 +258,7 @@ describe("Moves - Substitute", () => {
it(
"should protect the user from being afflicted with status effects",
async () => {
game.override.enemyMoveset(Array(4).fill(Moves.NUZZLE));
game.override.enemyMoveset(Moves.NUZZLE);
await game.classicMode.startBattle([Species.BLASTOISE]);
@ -276,7 +277,7 @@ describe("Moves - Substitute", () => {
it(
"should prevent the user's items from being stolen",
async () => {
game.override.enemyMoveset(Array(4).fill(Moves.THIEF));
game.override.enemyMoveset(Moves.THIEF);
vi.spyOn(allMoves[Moves.THIEF], "attrs", "get").mockReturnValue([new StealHeldItemChanceAttr(1.0)]); // give Thief 100% steal rate
game.override.startingHeldItems([{name: "BERRY", type: BerryType.SITRUS}]);
@ -318,7 +319,7 @@ describe("Moves - Substitute", () => {
it(
"move effect should prevent the user's berries from being stolen and eaten",
async () => {
game.override.enemyMoveset(Array(4).fill(Moves.BUG_BITE));
game.override.enemyMoveset(Moves.BUG_BITE);
game.override.startingHeldItems([{name: "BERRY", type: BerryType.SITRUS}]);
await game.classicMode.startBattle([Species.BLASTOISE]);
@ -343,7 +344,7 @@ describe("Moves - Substitute", () => {
it(
"should prevent the user's stats from being reset by Clear Smog",
async () => {
game.override.enemyMoveset(Array(4).fill(Moves.CLEAR_SMOG));
game.override.enemyMoveset(Moves.CLEAR_SMOG);
await game.classicMode.startBattle([Species.BLASTOISE]);
@ -362,7 +363,7 @@ describe("Moves - Substitute", () => {
it(
"should prevent the user from becoming confused",
async () => {
game.override.enemyMoveset(Array(4).fill(Moves.MAGICAL_TORQUE));
game.override.enemyMoveset(Moves.MAGICAL_TORQUE);
vi.spyOn(allMoves[Moves.MAGICAL_TORQUE], "chance", "get").mockReturnValue(100);
await game.classicMode.startBattle([Species.BLASTOISE]);
@ -408,7 +409,7 @@ describe("Moves - Substitute", () => {
it(
"should prevent the source's Rough Skin from activating when hit",
async () => {
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
game.override.enemyMoveset(Moves.TACKLE);
game.override.ability(Abilities.ROUGH_SKIN);
await game.classicMode.startBattle([Species.BLASTOISE]);
@ -426,7 +427,7 @@ describe("Moves - Substitute", () => {
it(
"should prevent the source's Focus Punch from failing when hit",
async () => {
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
game.override.enemyMoveset(Moves.TACKLE);
game.override.moveset([Moves.FOCUS_PUNCH]);
// Make Focus Punch 40 power to avoid a KO
@ -451,7 +452,7 @@ describe("Moves - Substitute", () => {
it(
"should not allow Shell Trap to activate when attacked",
async () => {
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
game.override.enemyMoveset(Moves.TACKLE);
game.override.moveset([Moves.SHELL_TRAP]);
await game.classicMode.startBattle([Species.BLASTOISE]);
@ -471,7 +472,7 @@ describe("Moves - Substitute", () => {
it(
"should not allow Beak Blast to burn opponents when hit",
async () => {
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
game.override.enemyMoveset(Moves.TACKLE);
game.override.moveset([Moves.BEAK_BLAST]);
await game.classicMode.startBattle([Species.BLASTOISE]);
@ -491,8 +492,8 @@ describe("Moves - Substitute", () => {
it(
"should cause incoming attacks to not activate Counter",
async() => {
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
async () => {
game.override.enemyMoveset(Moves.TACKLE);
game.override.moveset([Moves.COUNTER]);
await game.classicMode.startBattle([Species.BLASTOISE]);
@ -510,4 +511,25 @@ describe("Moves - Substitute", () => {
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
}
);
it(
"should prevent Sappy Seed from applying its Leech Seed effect to the user",
async () => {
game.override.enemyMoveset(Moves.SAPPY_SEED);
await game.classicMode.startBattle([Species.CHARIZARD]);
const playerPokemon = game.scene.getPlayerPokemon()!;
playerPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, playerPokemon.id);
game.move.select(Moves.SPLASH);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); // enemy uses Sappy Seed first
await game.move.forceHit(); // forces Sappy Seed to hit
await game.phaseInterceptor.to("MoveEndPhase");
expect(playerPokemon.getTag(BattlerTagType.SEEDED)).toBeUndefined();
}
);
});